"""Alumni "Impact / Tracer Survey" wrapper around the M&EL survey engine.

This module does NOT reimplement survey/dispatch/analysis logic — it builds
an alumni cohort audience, then delegates entirely to
:mod:`apps.mel.feedback.services` (``create_survey``, ``activate_survey``,
``dispatch_survey``) so responses land in the same structured Survey /
SurveyResponse / SatisfactionScore pipeline as every other M&EL survey, and
``analyse_responses`` / the results dashboard work unchanged.

See ``apps.alumni.profiles.services_tracer.launch_tracer_survey`` for the
older, unstructured precedent (``TracerStudy``) this supersedes for new
tracer launches — that primitive is left untouched for existing callers.
"""
from __future__ import annotations

import logging
from typing import Any

from django.db import transaction
from django.utils.text import slugify

from apps.mel.feedback.models import Survey, SurveyQuestionType, SurveyTrigger
from apps.mel.feedback.services import activate_survey, create_survey, dispatch_survey

logger = logging.getLogger(__name__)

# Deterministic tag on every Survey launched through this wrapper — lets the
# officer UI list "alumni impact surveys" distinctly from other M&EL surveys
# without adding a field to the shared engine's Survey model.
IMPACT_SURVEY_SLUG_PREFIX = "alumni-impact-"

DEFAULT_TRACER_QUESTIONS: list[dict[str, Any]] = [
    {
        "type": SurveyQuestionType.MULTIPLE_CHOICE,
        "text": "What is your current employment status?",
        "required": True,
        "options": [
            {"value": "employed_ft", "label": "Employed full-time"},
            {"value": "employed_pt", "label": "Employed part-time"},
            {"value": "self_employed", "label": "Self-employed / running a business"},
            {"value": "further_study", "label": "In further study"},
            {"value": "unemployed", "label": "Seeking employment"},
            {"value": "other", "label": "Other"},
        ],
    },
    {
        "type": SurveyQuestionType.SHORT_ANSWER,
        "text": "What sector do you currently work in?",
        "required": False,
    },
    {
        "type": SurveyQuestionType.SHORT_ANSWER,
        "text": "What is your current job title?",
        "required": False,
    },
    {
        "type": SurveyQuestionType.MULTIPLE_CHOICE,
        "text": "How has your income changed since graduating?",
        "required": False,
        "options": [
            {"value": "increased_significantly", "label": "Increased significantly"},
            {"value": "increased_slightly", "label": "Increased slightly"},
            {"value": "no_change", "label": "No change"},
            {"value": "decreased", "label": "Decreased"},
            {"value": "not_applicable", "label": "Not applicable"},
        ],
    },
    {
        "type": SurveyQuestionType.BOOLEAN,
        "text": "Have you undertaken further study since graduating?",
        "required": False,
    },
    {
        "type": SurveyQuestionType.RATING,
        "text": "How relevant was your RUFORUM training to your current work?",
        "required": True,
        "options": {"min": 1, "max": 5},
    },
    {
        "type": SurveyQuestionType.ESSAY,
        "text": (
            "Tell us about the impact your RUFORUM training has had on your "
            "career or community (an impact story)."
        ),
        "required": False,
    },
]


def _unique_impact_slug(title: str, *, exclude_pk: int | None = None) -> str:
    """Deterministic, prefix-tagged, unique Survey slug.

    No time-based or random component — collisions are resolved with a
    stable numeric suffix against current DB state, mirroring the engine's
    own ``_slugify_unique`` helper in ``apps.mel.feedback.services``.
    """
    base = slugify(f"{IMPACT_SURVEY_SLUG_PREFIX}{title}")[:130] or f"{IMPACT_SURVEY_SLUG_PREFIX}tracer-survey"
    slug = base
    n = 2
    qs = Survey.objects.all()
    if exclude_pk is not None:
        qs = qs.exclude(pk=exclude_pk)
    while qs.filter(slug=slug).exists():
        slug = f"{base}-{n}"[:140]
        n += 1
    return slug


def alumni_cohort_queryset(cohort_filter: dict[str, Any] | None):
    """Build the ``AlumniProfile`` queryset for a cohort filter.

    Mirrors ``apps.alumni.profiles.services_tracer.launch_tracer_survey``'s
    filtering exactly, so the two launchers agree on what a cohort means.
    Recognised keys: ``graduation_year`` (int), ``programme_name`` (str,
    icontains), ``country`` (str, iexact against ``UserProfile.country``).
    All are optional — an empty/None filter targets every alumnus.
    """
    from apps.alumni.profiles.models import AlumniProfile

    cohort_filter = cohort_filter or {}
    year = cohort_filter.get("graduation_year")
    programme = cohort_filter.get("programme_name")
    country = cohort_filter.get("country")

    qs = AlumniProfile.objects.all().select_related("user")
    if year:
        qs = qs.filter(degrees__graduated_on__year=year).distinct()
    if programme:
        qs = qs.filter(degrees__programme_name__icontains=programme).distinct()
    if country:
        qs = qs.filter(user__profile__country__iexact=country)
    return qs


@transaction.atomic
def launch_impact_survey(
    *,
    title: str,
    description: str = "",
    cohort_filter: dict[str, Any] | None = None,
    questions: list[dict] | None = None,
    created_by=None,
) -> Survey:
    """Create, activate and dispatch an alumni impact/tracer Survey.

    Builds the alumni cohort from ``cohort_filter`` (see
    :func:`alumni_cohort_queryset`), creates a draft ``Survey`` with
    ``questions`` (defaults to :data:`DEFAULT_TRACER_QUESTIONS`), activates
    it, then dispatches to every matching alumnus's user account via
    ``apps.mel.feedback.services.dispatch_survey`` — which itself handles
    token generation and invite delivery, unchanged.

    Returns the created (activated, dispatched) ``Survey``.
    """
    cohort_filter = cohort_filter or {}
    question_specs = questions if questions is not None else DEFAULT_TRACER_QUESTIONS

    survey = create_survey(
        title=title,
        description=description,
        trigger=SurveyTrigger.POST_GRADUATION,
        audience={"kind": "alumni_impact", "cohort_filter": cohort_filter},
        is_anonymous=False,
        questions=question_specs,
        created_by=created_by,
    )
    # Retag the slug so the officer list can reliably filter these surveys
    # apart from other M&EL surveys (see IMPACT_SURVEY_SLUG_PREFIX).
    tagged_slug = _unique_impact_slug(title, exclude_pk=survey.pk)
    if survey.slug != tagged_slug:
        survey.slug = tagged_slug
        survey.save(update_fields=["slug"])

    activate_survey(survey)

    profile_qs = alumni_cohort_queryset(cohort_filter)
    user_ids = list(profile_qs.values_list("user_id", flat=True).distinct())
    from django.contrib.auth import get_user_model

    User = get_user_model()
    recipients = User.objects.filter(pk__in=user_ids)

    dispatch_survey(
        survey,
        recipients=recipients,
        trigger_event=f"alumni_impact_{survey.slug}",
    )
    logger.info(
        "alumni.impact_survey launched slug=%s cohort=%s recipients=%s",
        survey.slug, cohort_filter, len(user_ids),
    )
    return survey


# ---------------------------------------------------------------------------
# Cross-survey insight aggregation (for the alumni impact report dashboard)
# ---------------------------------------------------------------------------

# Question text is the stable join key across surveys — every impact survey is
# built from DEFAULT_TRACER_QUESTIONS, so the same text appears in each, even
# though per-survey question PKs differ.
_Q_EMPLOYMENT = "What is your current employment status?"
_Q_SECTOR = "What sector do you currently work in?"
_Q_INCOME = "How has your income changed since graduating?"
_Q_FURTHER_STUDY = "Have you undertaken further study since graduating?"
_Q_RELEVANCE = "How relevant was your RUFORUM training to your current work?"

_EMPLOYMENT_LABELS = {
    o["value"]: o["label"] for o in DEFAULT_TRACER_QUESTIONS[0]["options"]
}
_INCOME_LABELS = {
    o["value"]: o["label"] for o in DEFAULT_TRACER_QUESTIONS[3]["options"]
}
# Which employment states count as a positive labour-market outcome.
_POSITIVE_EMPLOYMENT = {"employed_ft", "employed_pt", "self_employed"}


def _distribution(counter: dict[str, int], labels: dict[str, str]):
    """Turn a {value: count} counter into ordered rows with percentages."""
    total = sum(counter.values())
    rows = [
        {
            "value": value,
            "label": labels.get(value, value.replace("_", " ").title()),
            "count": count,
            "pct": round((count / total) * 100, 1) if total else 0.0,
        }
        for value, count in counter.items()
    ]
    rows.sort(key=lambda r: -r["count"])
    return rows


def aggregate_tracer_insights(limit_stories: int = 4) -> dict[str, Any]:
    """Aggregate completed responses across ALL alumni impact/tracer surveys.

    Returns donor-ready insight blocks — employment outcomes, income change,
    further-study rate, mean training relevance, top sectors and recent impact
    stories — so the impact report can show what the tracer studies actually
    found, not just that they ran.
    """
    from apps.mel.feedback.models import (
        Survey,
        SurveyAnswer,
        SurveyDispatch,
        SurveyResponse,
    )

    survey_ids = list(
        Survey.objects.filter(slug__startswith=IMPACT_SURVEY_SLUG_PREFIX).values_list(
            "pk", flat=True
        )
    )
    empty = {
        "has_data": False,
        "surveys_launched": len(survey_ids),
        "total_dispatched": 0,
        "total_completed": 0,
        "response_rate": 0.0,
    }
    if not survey_ids:
        return empty

    total_dispatched = SurveyDispatch.objects.filter(survey_id__in=survey_ids).count()
    completed_qs = SurveyResponse.objects.filter(
        survey_id__in=survey_ids, status=SurveyResponse.Status.COMPLETED
    )
    total_completed = completed_qs.count()
    if total_completed == 0:
        empty["total_dispatched"] = total_dispatched
        return empty

    answers = (
        SurveyAnswer.objects.filter(
            response__survey_id__in=survey_ids,
            response__status=SurveyResponse.Status.COMPLETED,
        )
        .select_related("question")
        .values_list("question__text", "value", "free_text")
    )

    employment: dict[str, int] = {}
    income: dict[str, int] = {}
    sectors: dict[str, int] = {}
    further_yes = further_total = 0
    ratings: list[int] = []
    for q_text, value, free_text in answers:
        v = (value or {}).get("value") if isinstance(value, dict) else value
        if q_text == _Q_EMPLOYMENT and v:
            employment[v] = employment.get(v, 0) + 1
        elif q_text == _Q_INCOME and v:
            income[v] = income.get(v, 0) + 1
        elif q_text == _Q_FURTHER_STUDY and v is not None:
            further_total += 1
            if str(v).lower() in ("true", "1", "yes"):
                further_yes += 1
        elif q_text == _Q_RELEVANCE and v not in (None, ""):
            try:
                ratings.append(int(v))
            except (TypeError, ValueError):
                pass
        elif q_text == _Q_SECTOR and (free_text or "").strip():
            key = free_text.strip().title()
            sectors[key] = sectors.get(key, 0) + 1

    employment_rows = _distribution(employment, _EMPLOYMENT_LABELS)
    positive_pct = round(
        sum(r["pct"] for r in employment_rows if r["value"] in _POSITIVE_EMPLOYMENT), 1
    )
    top_sectors = sorted(sectors.items(), key=lambda kv: -kv[1])[:6]

    stories = list(
        SurveyAnswer.objects.filter(
            response__survey_id__in=survey_ids,
            response__status=SurveyResponse.Status.COMPLETED,
            question__type="essay",
        )
        .exclude(free_text="")
        .select_related("response")
        .order_by("-response__completed_at")[:limit_stories]
        .values_list("free_text", flat=True)
    )

    return {
        "has_data": True,
        "surveys_launched": len(survey_ids),
        "total_dispatched": total_dispatched,
        "total_completed": total_completed,
        "response_rate": round((total_completed / total_dispatched) * 100, 1)
        if total_dispatched
        else 0.0,
        "employment": employment_rows,
        "employed_pct": positive_pct,
        "income": _distribution(income, _INCOME_LABELS),
        "further_study_yes_pct": round((further_yes / further_total) * 100, 1)
        if further_total
        else 0.0,
        "further_study_n": further_total,
        "training_relevance_mean": round(sum(ratings) / len(ratings), 1)
        if ratings
        else None,
        "training_relevance_n": len(ratings),
        "top_sectors": [{"label": k, "count": v} for k, v in top_sectors],
        "stories": [s.strip() for s in stories],
    }
