"""Shared pipeline-metric classification — the single source of truth for
which "requirement owner" metric (submitted/shortlisted/offered/joined/
offer_rejected/rejected) a JobApplication counts toward.

Used by BOTH:
  - apps.dashboard.pm_views.ProjectManagerDashboardView  (the KPI counts)
  - apps.candidates.views.CandidateListCreateAPIView      (the `pm_metric`
    drill-down filter, so clicking a dashboard card shows exactly the
    candidates behind that card's count — same query, same numbers, always
    computed fresh from the current pipeline state.)

"Reached or beyond" counts WON candidates and post-milestone LOST candidates
(e.g. an offer-decliner certainly reached `submitted`), so numbers never
shrink as candidates progress.
"""

from .models import JobApplication, PipelineStage

# LOST stages that can only happen after these milestones
OFFER_LOST = {"offer_declined", "offered_not_joined"}
POST_SUBMIT_LOST = OFFER_LOST | {"client_declined"}

METRIC_KEYS = ("submitted", "shortlisted", "offered", "joined", "offer_rejected", "rejected")


def get_thresholds():
    """sort_order of the submitted/selected/offered stages, keyed by metric name."""
    stage_by_code = {s.code: s for s in PipelineStage.objects.filter(is_active=True)}
    return {
        "submitted": getattr(stage_by_code.get("submitted"), "sort_order", None),
        "shortlisted": getattr(stage_by_code.get("selected"), "sort_order", None),
        "offered": getattr(stage_by_code.get("offered"), "sort_order", None),
    }


def classify(stage, thresholds):
    """Which metrics one application (at its current stage) counts toward."""
    if stage is None:
        return ()
    lost = stage.outcome == PipelineStage.Outcome.LOST
    won = stage.outcome == PipelineStage.Outcome.WON
    hit = []

    def reached(threshold, lost_codes):
        if won:
            return True
        if lost:
            return stage.code in lost_codes
        return threshold is not None and stage.sort_order >= threshold

    if reached(thresholds["submitted"], POST_SUBMIT_LOST):
        hit.append("submitted")
    if reached(thresholds["shortlisted"], OFFER_LOST):
        hit.append("shortlisted")
    if reached(thresholds["offered"], OFFER_LOST):
        hit.append("offered")
    if won:
        hit.append("joined")
    if lost and stage.code in OFFER_LOST:
        hit.append("offer_rejected")
    if lost:
        hit.append("rejected")
    return hit


def candidate_ids_for_metric(job_ids, metric, date_from=None, date_to=None):
    """Live candidate ids matching `metric` (one of METRIC_KEYS) across the
    given job_ids — computed fresh, with the exact same classification the
    PM/TA dashboards' counts use, so a count and its drill-down can never
    disagree.

    `job_ids=None` means "every job" (no job-ownership scoping) — used by the
    org-wide TA dashboard. `date_from`/`date_to` optionally restrict to
    applications *created* within a window (the TA dashboard's period filter);
    left as None (the PM dashboard's usage) this is unrestricted, exactly as
    before.
    """
    if metric not in METRIC_KEYS:
        return []
    thresholds = get_thresholds()
    applications = JobApplication.objects.filter(candidate__is_deleted=False).select_related("stage")
    if job_ids is not None:
        applications = applications.filter(job_id__in=job_ids)
    if date_from:
        applications = applications.filter(created_at__date__gte=date_from)
    if date_to:
        applications = applications.filter(created_at__date__lte=date_to)
    ids = set()
    for app in applications:
        if metric in classify(app.stage, thresholds):
            ids.add(app.candidate_id)
    return sorted(ids)


def candidate_ids_for_stage_codes(stage_codes, job_ids=None):
    """Live candidate ids whose CURRENT stage is exactly one of `stage_codes`
    (no "reached or beyond" — an exact snapshot of where they sit right now).

    This is the classification the Recruiter and Admin dashboards' KPIs use
    (e.g. "Offered" = current stage is literally `offered` or `placed`), as
    opposed to the PM/TA dashboards' cumulative `classify()` above. Keeping
    both here, side by side, makes the difference explicit and keeps every
    drill-down's candidate_ids computation in one auditable place.
    """
    if not stage_codes:
        return []
    applications = JobApplication.objects.filter(
        candidate__is_deleted=False, stage__code__in=stage_codes,
    )
    if job_ids is not None:
        applications = applications.filter(job_id__in=job_ids)
    return sorted(set(applications.values_list("candidate_id", flat=True)))
