from __future__ import annotations

import logging
from datetime import date
from decimal import Decimal
from typing import Any

from django.conf import settings
from django.db import transaction
from django.db.models import Count, Q, Sum
from django.utils import timezone

from apps.alumni.profiles.models import AlumniProfile
from apps.alumni.tracking.models import (
    CareerMilestone,
    ImpactMetric,
    ImpactRecord,
    ImpactSnapshot,
    MilestoneType,
    OrcidSyncLog,
    ResearchOutput,
)
from apps.alumni.tracking.orcid import (
    OrcidError,
    OrcidNotFound,
    fetch_works,
)

logger = logging.getLogger(__name__)


@transaction.atomic
def record_milestone(
    profile: AlumniProfile,
    *,
    milestone_type: str,
    title: str,
    occurred_on: date,
    description: str = "",
    url: str = "",
    visibility: str = "public",
    evidence_file=None,
) -> CareerMilestone:
    milestone = CareerMilestone.objects.create(
        profile=profile,
        milestone_type=milestone_type,
        title=title,
        occurred_on=occurred_on,
        description=description,
        url=url,
        visibility=visibility,
        evidence_file=evidence_file,
    )
    _notify_milestone(profile, milestone)
    _record_to_mel(
        "alumni-milestones-recorded",
        "alumni",
        "milestone_recorded",
        str(milestone.pk),
    )
    return milestone


def _notify_milestone(profile: AlumniProfile, milestone: CareerMilestone) -> None:
    try:
        from apps.core.notifications.models import Notification
        from apps.core.notifications.services import send_notification

        send_notification(
            profile.user,
            f"Milestone recorded: {milestone.title}",
            verb=Notification.Verb.ALUMNI_MILESTONE_ADDED,
        )
    except Exception:  # pragma: no cover - defensive
        logger.exception("alumni.milestone notify failed milestone=%s", milestone.pk)


@transaction.atomic
def sync_orcid(profile: AlumniProfile) -> OrcidSyncLog:
    """Fetch ORCID works and upsert :class:`ResearchOutput` rows.

    Never raises — failures land on :class:`OrcidSyncLog` with an incremented
    ``consecutive_failures`` counter, and after a configurable threshold we
    notify the profile owner once.
    """
    orcid_id = (profile.orcid_id or "").strip()
    log, _ = OrcidSyncLog.objects.get_or_create(
        profile=profile,
        defaults={"orcid_id": orcid_id},
    )
    if not orcid_id:
        return log

    log.orcid_id = orcid_id
    log.last_attempt_at = timezone.now()
    try:
        works = fetch_works(
            orcid_id,
            base_url=getattr(settings, "ORCID_API_BASE_URL", "https://pub.orcid.org"),
            timeout=int(getattr(settings, "ORCID_API_TIMEOUT", 15)),
        )
    except OrcidNotFound as exc:
        log.last_error = f"not_found: {exc}"
        log.consecutive_failures += 1
        log.save(
            update_fields=[
                "orcid_id",
                "last_attempt_at",
                "last_error",
                "consecutive_failures",
            ]
        )
        _maybe_alert_sync_failure(profile, log)
        return log
    except OrcidError as exc:
        log.last_error = f"orcid: {exc}"
        log.consecutive_failures += 1
        log.save(
            update_fields=[
                "orcid_id",
                "last_attempt_at",
                "last_error",
                "consecutive_failures",
            ]
        )
        _maybe_alert_sync_failure(profile, log)
        return log

    imported = 0
    for work in works:
        _, created = ResearchOutput.objects.update_or_create(
            profile=profile,
            put_code=work.put_code,
            defaults={
                "title": work.title,
                "publication_type": work.publication_type,
                "doi": work.doi,
                "year": work.year,
                "payload": work.payload,
            },
        )
        if created:
            imported += 1

    log.last_success_at = timezone.now()
    log.last_error = ""
    log.consecutive_failures = 0
    log.works_imported_count = log.works_imported_count + imported
    log.save(
        update_fields=[
            "orcid_id",
            "last_attempt_at",
            "last_success_at",
            "last_error",
            "consecutive_failures",
            "works_imported_count",
        ]
    )
    _record_to_mel(
        "alumni-orcid-synced",
        "alumni",
        "orcid_synced",
        str(profile.pk),
        value=imported,
    )
    return log


def _maybe_alert_sync_failure(profile: AlumniProfile, log: OrcidSyncLog) -> None:
    threshold = int(getattr(settings, "ALUMNI_ORCID_FAILURE_ALERT_THRESHOLD", 3))
    if log.consecutive_failures != threshold:
        return
    try:
        from apps.core.notifications.models import Notification
        from apps.core.notifications.services import send_notification

        send_notification(
            profile.user,
            "We couldn't sync your ORCID works after several attempts — please confirm your ORCID iD.",
            verb=Notification.Verb.ALUMNI_ORCID_SYNC_FAILED,
        )
    except Exception:  # pragma: no cover - defensive
        logger.exception("alumni.orcid notify failed profile=%s", profile.pk)


def compute_impact_summary(profile: AlumniProfile) -> dict[str, Any]:
    milestones = profile.milestones.count()
    publications = profile.research_outputs.count() + profile.publications.count()
    by_metric = {
        row["metric"]: float(row["total"] or 0)
        for row in profile.impact_records.values("metric").annotate(total=Sum("value"))
    }
    return {
        "milestones": milestones,
        "publications": publications,
        "metrics": by_metric,
    }


def current_period_label(today: date | None = None) -> str:
    d = today or timezone.now().date()
    return f"{d.year}-{((d.month - 1) // 3) + 1:d}".replace("-", "-Q")


@transaction.atomic
def build_impact_snapshot(period_label: str) -> ImpactSnapshot:
    total_alumni = AlumniProfile.objects.count()
    from apps.alumni.engagement.models import (  # local import to avoid cycle
        MentorshipPairing,
    )

    active_mentorships = MentorshipPairing.objects.filter(status="active").count()
    total_publications = ResearchOutput.objects.count()
    total_citations = int(
        (
            ImpactRecord.objects.filter(metric=ImpactMetric.CITATIONS).aggregate(
                s=Sum("value")
            )["s"]
            or 0
        )
    )
    funding_secured_usd = (
        ImpactRecord.objects.filter(metric=ImpactMetric.FUNDING_SECURED_USD).aggregate(
            s=Sum("value")
        )["s"]
        or Decimal("0")
    )

    breakdown = {
        "by_country": list(
            AlumniProfile.objects.values("user__profile__country")
            .exclude(Q(user__profile__country__isnull=True) | Q(user__profile__country=""))
            .annotate(total=Count("pk"))
            .order_by("-total")[:20]
        ),
        "by_source": list(
            AlumniProfile.objects.values("source").annotate(total=Count("pk"))
        ),
    }
    snapshot, _ = ImpactSnapshot.objects.update_or_create(
        period_label=period_label,
        defaults={
            "total_alumni": total_alumni,
            "active_mentorships": active_mentorships,
            "total_publications": total_publications,
            "total_citations": total_citations,
            "funding_secured_usd": funding_secured_usd,
            "breakdown": breakdown,
        },
    )
    return snapshot


def generate_donor_impact_report(
    period_label: str | None = None,
) -> dict[str, Any]:
    label = period_label or current_period_label()
    try:
        snapshot = ImpactSnapshot.objects.get(period_label=label)
    except ImpactSnapshot.DoesNotExist:
        snapshot = build_impact_snapshot(label)
    return {
        "period_label": snapshot.period_label,
        "total_alumni": snapshot.total_alumni,
        "active_mentorships": snapshot.active_mentorships,
        "total_publications": snapshot.total_publications,
        "total_citations": snapshot.total_citations,
        "funding_secured_usd": float(snapshot.funding_secured_usd),
        "breakdown": snapshot.breakdown,
        "history": list(
            ImpactSnapshot.objects.order_by("-period_label").values(
                "period_label",
                "total_alumni",
                "active_mentorships",
                "total_publications",
            )[:12]
        ),
    }


def _record_to_mel(
    indicator_code: str,
    source_module: str,
    source_event: str,
    source_id: str,
    *,
    value: Any = 1,
) -> None:
    try:
        from apps.mel.indicators.services import record_automated_point

        record_automated_point(
            indicator_code=indicator_code,
            source_module=source_module,
            source_event=source_event,
            source_object_id=source_id,
            value=value,
        )
    except Exception:  # pragma: no cover - defensive
        logger.exception(
            "mel.indicator record failed indicator=%s event=%s",
            indicator_code,
            source_event,
        )
