"""Alumni handoff delivery (FRREP-TS007).

The ``AlumniHandoff`` row is the system-of-record for "a learner finished a
programme → notify the Alumni system". Historically the REP LMS produced these
on course completion; with learning moved to Moodle, the producer is gone and a
Moodle-completion producer can call :func:`create_alumni_handoff` in future. The
send/retry transport moved here (out of the deleted ``apps.rep.courses``) so the
capability — and the ``retry_pending_alumni_handoffs`` beat — survive intact.
"""
from __future__ import annotations

import hashlib
import hmac
import json
import logging
from typing import Any

from django.conf import settings
from django.utils import timezone

from apps.alumni.careers.models import AlumniHandoff

logger = logging.getLogger(__name__)


def create_alumni_handoff(payload: dict[str, Any]) -> AlumniHandoff | None:
    """Persist a PENDING handoff and attempt delivery when configured.

    ``ALUMNI_WEBHOOK_URL`` unset → row stays PENDING (visible to operators).
    Returns the row, or ``None`` when the payload lacks ``learner_id``.
    """
    learner_pk = payload.get("learner_id")
    if not learner_pk:
        logger.warning("alumni.handoff: payload missing learner_id; skipping")
        return None

    handoff = AlumniHandoff.objects.create(
        learner_id=learner_pk,
        payload=payload,
        status=AlumniHandoff.Status.PENDING,
    )

    if getattr(settings, "ALUMNI_WEBHOOK_URL", "") or "":
        _attempt_alumni_post(handoff)
    return handoff


def _attempt_alumni_post(handoff: AlumniHandoff) -> None:
    """Single POST attempt against the configured Alumni webhook; mutates the
    handoff row to reflect the outcome."""
    url = getattr(settings, "ALUMNI_WEBHOOK_URL", "") or ""
    secret = getattr(settings, "ALUMNI_WEBHOOK_SECRET", "") or ""

    handoff.attempts = (handoff.attempts or 0) + 1
    body = json.dumps(handoff.payload).encode()

    try:
        import urllib.error  # noqa: F401
        import urllib.request

        req = urllib.request.Request(
            url,
            data=body,
            method="POST",
            headers={"Content-Type": "application/json"},
        )
        if secret:
            req.add_header(
                "X-Ruforum-Signature",
                hmac.new(secret.encode(), body, hashlib.sha256).hexdigest(),
            )
        with urllib.request.urlopen(req, timeout=30) as resp:  # noqa: S310
            status = getattr(resp, "status", 200)
            if status >= 400:
                raise RuntimeError(f"HTTP {status}")
            response_body = resp.read() or b""
    except Exception as exc:
        handoff.status = AlumniHandoff.Status.FAILED
        handoff.last_error = str(exc)[:2000]
        handoff.save(update_fields=["status", "last_error", "attempts"])
        logger.warning(
            "alumni.handoff failed (attempt %s/%s) handoff=%s: %s",
            handoff.attempts, AlumniHandoff.MAX_ATTEMPTS, handoff.pk, exc,
        )
        return

    reference = ""
    if response_body:
        try:
            decoded = json.loads(response_body.decode("utf-8"))
            if isinstance(decoded, dict):
                reference = str(decoded.get("reference_id") or "")[:120]
        except (ValueError, UnicodeDecodeError):
            reference = ""

    handoff.last_error = ""
    handoff.sent_at = timezone.now()
    if reference:
        handoff.alumni_module_reference = reference
        handoff.confirmed_at = timezone.now()
        handoff.status = AlumniHandoff.Status.CONFIRMED
    else:
        handoff.status = AlumniHandoff.Status.SENT
    handoff.save(update_fields=[
        "status", "attempts", "last_error", "sent_at",
        "confirmed_at", "alumni_module_reference",
    ])


def retry_failed_alumni_handoffs(*, max_per_run: int = 50) -> int:
    """Re-attempt every FAILED handoff under its retry budget; returns the count
    attempted. No-op when ``ALUMNI_WEBHOOK_URL`` is unset."""
    if not (getattr(settings, "ALUMNI_WEBHOOK_URL", "") or ""):
        return 0

    candidates = (
        AlumniHandoff.objects.filter(status=AlumniHandoff.Status.FAILED)
        .order_by("created_at")[:max_per_run]
    )
    count = 0
    for handoff in candidates:
        if handoff.attempts >= AlumniHandoff.MAX_ATTEMPTS:
            continue
        _attempt_alumni_post(handoff)
        count += 1
    return count
