"""Alumni profile services.

All write paths are idempotent by ``user_id`` (one profile per user, regardless
of how many handoff sources fire). Exceptions are logged and swallowed in the
upstream caller so they cannot roll back an award / enrolment transaction.
"""
from __future__ import annotations

import logging
from typing import Any

from django.contrib.auth import get_user_model
from django.db import transaction
from django.utils import timezone

from apps.alumni.profiles.models import (
    AlumniCertificate,
    AlumniProfile,
    AlumniSource,
    Degree,
    DegreeType,
    Publication,
)
from apps.alumni.profiles.signals import (
    alumni_profile_created,
    alumni_profile_visibility_changed,
)

logger = logging.getLogger(__name__)

User = get_user_model()


_DEGREE_TYPE_BY_LEVEL = {
    "phd": DegreeType.PHD,
    "masters": DegreeType.MSC,
    "undergraduate": DegreeType.BSC,
    "other": DegreeType.OTHER,
}


def _resolve_degree_type(raw: str | None) -> str:
    if not raw:
        return DegreeType.OTHER
    normalised = raw.strip().lower()
    if normalised in _DEGREE_TYPE_BY_LEVEL:
        return _DEGREE_TYPE_BY_LEVEL[normalised]
    for code, _ in DegreeType.choices:
        if normalised == code.lower():
            return code
    return DegreeType.OTHER


def _merge_source(profile: AlumniProfile, incoming: str) -> str:
    """Collapse multiple handoff sources into MIXED when they diverge."""
    current = profile.source
    if current == incoming:
        return current
    if current in (AlumniSource.MANUAL, AlumniSource.CSV_IMPORT):
        return incoming
    return AlumniSource.MIXED


def _append_source_ref(profile: AlumniProfile, key: str, value: Any) -> None:
    bucket = profile.source_refs.setdefault(key, [])
    if value in bucket:
        return
    bucket.append(value)


@transaction.atomic
def create_from_graduation(graduation_record) -> AlumniProfile:
    """Promote a RIMS ``GraduationRecord`` into an alumni profile.

    Idempotent: re-calling with the same graduation record is a no-op beyond
    accumulating the reference in ``source_refs``.
    """
    scholar = getattr(graduation_record, "scholar", None)
    if scholar is None:
        raise ValueError("graduation_record missing scholar")
    user = scholar.user

    profile, created = AlumniProfile.objects.get_or_create(
        user=user,
        defaults={
            "source": AlumniSource.RIMS_SCHOLARSHIP,
            "source_refs": {"graduation_ids": [graduation_record.pk]},
            "current_institution": getattr(scholar, "institution", None),
        },
    )
    changed_fields = []
    if not created:
        new_source = _merge_source(profile, AlumniSource.RIMS_SCHOLARSHIP)
        if profile.source != new_source:
            profile.source = new_source
            changed_fields.append("source")
        _append_source_ref(profile, "graduation_ids", graduation_record.pk)
        changed_fields.append("source_refs")
        if profile.current_institution_id is None and getattr(scholar, "institution_id", None):
            profile.current_institution = scholar.institution
            changed_fields.append("current_institution")
        if changed_fields:
            profile.save(update_fields=list(set(changed_fields + ["updated_at"])))

    programme_name = getattr(scholar.scholarship, "name", "") or "RUFORUM programme"
    degree_level = getattr(getattr(user, "profile", None), "degree_level", "") or ""
    Degree.objects.get_or_create(
        profile=profile,
        programme_name=programme_name,
        graduated_on=graduation_record.graduated_on,
        defaults={
            "institution": getattr(scholar, "institution", None),
            "degree_type": _resolve_degree_type(degree_level),
            "funder": getattr(
                getattr(scholar.scholarship, "partner", None), "name", ""
            )
            or "",
        },
    )

    if created:
        alumni_profile_created.send(
            sender=AlumniProfile,
            profile=profile,
            source=AlumniSource.RIMS_SCHOLARSHIP,
        )
        _record_profile_to_mel(profile, event_type="alumni_profile_created")

    return profile


@transaction.atomic
def create_from_rep_completion(payload: dict) -> AlumniProfile | None:
    """Apply one REP ``IntegrationOutbox`` course_completion row.

    Payload shape matches ``apps/rep/courses/integration.py:build_course_completion_payload``.
    Returns the profile (or ``None`` if the learner cannot be resolved).
    """
    learner_id = payload.get("learner_id")
    if not learner_id:
        logger.warning("alumni.create_from_rep_completion missing learner_id payload=%s", payload)
        return None
    try:
        user = User.objects.get(pk=learner_id)
    except User.DoesNotExist:
        logger.warning("alumni.create_from_rep_completion unknown learner_id=%s", learner_id)
        return None

    profile, created = AlumniProfile.objects.get_or_create(
        user=user,
        defaults={
            "source": AlumniSource.REP_COMPLETION,
            "source_refs": {"rep_enrolment_ids": [payload.get("enrolment_id")]},
        },
    )
    if not created:
        new_source = _merge_source(profile, AlumniSource.REP_COMPLETION)
        profile.source = new_source
        _append_source_ref(profile, "rep_enrolment_ids", payload.get("enrolment_id"))
        profile.save(update_fields=["source", "source_refs", "updated_at"])

    cert_number = payload.get("certificate_number")
    if cert_number:
        AlumniCertificate.objects.get_or_create(
            profile=profile,
            rep_certificate_number=str(cert_number),
            defaults={
                "title": payload.get("course_title", "REP certificate"),
                "issuer": "RUFORUM REP",
            },
        )

    if created:
        alumni_profile_created.send(
            sender=AlumniProfile,
            profile=profile,
            source=AlumniSource.REP_COMPLETION,
        )
        _record_profile_to_mel(profile, event_type="alumni_profile_created")

    return profile


@transaction.atomic
def update_profile(profile: AlumniProfile, **fields) -> AlumniProfile:
    for key, value in fields.items():
        setattr(profile, key, value)
    profile.save()
    return profile


@transaction.atomic
def toggle_visibility(profile: AlumniProfile, consent: bool) -> AlumniProfile:
    if profile.visibility_consent == consent:
        return profile
    profile.visibility_consent = consent
    if consent and profile.published_at is None:
        profile.published_at = timezone.now()
    profile.save(update_fields=["visibility_consent", "published_at", "updated_at"])
    alumni_profile_visibility_changed.send(
        sender=AlumniProfile,
        profile=profile,
        is_published=consent,
    )
    return profile


def link_publication_to_repository(publication: Publication, document) -> Publication:
    publication.document = document
    publication.cited_in_repository = True
    publication.save(update_fields=["document", "cited_in_repository"])
    return publication


def _record_profile_to_mel(profile: AlumniProfile, *, event_type: str) -> None:
    """Feed a data point into M&EL ``alumni-profiles-created`` indicator."""
    try:
        from apps.mel.tracking.models import UpstreamEvent
        from apps.mel.tracking.services import _safe_record, log_event

        log_event(
            module=UpstreamEvent.Module.ALUMNI,
            event_type=event_type,
            source_object_id=f"alumni-{profile.pk}",
            payload={"user_id": profile.user_id, "source": profile.source},
            subject=profile.user,
        )
        _safe_record(
            "alumni-profiles-created",
            "alumni",
            event_type,
            str(profile.pk),
        )
    except Exception:  # pragma: no cover - defensive
        logger.exception("mel.log_event failed for alumni profile=%s", profile.pk)
