"""Celery tasks for the M&EL feedback domain (Phase 4.3 + G16 + G6).

Currently covers:
* :func:`scan_due_survey_reminders` — fires the pre-due reminder once when a
  dispatched survey has less than 48 h left in its response window (G16).
* :func:`scan_survey_non_responses` — sweeps SurveyDispatch rows past their
  ``due_at`` window without a completed response, escalating them to
  NON_RESPONDED so MEI019 metrics stay accurate.
* :func:`scan_post_project_tracers` — fires tracer studies tied to a closed
  log frame once its ``closed_at + post_project_window_months`` has elapsed
  (G6). Idempotent on TracerStudy.dispatched_at.
"""
from __future__ import annotations

import logging

from celery import shared_task
from django.db import models
from django.utils import timezone

logger = logging.getLogger(__name__)


# Window (in hours) before ``due_at`` at which the pre-due reminder fires.
PRE_DUE_REMINDER_HOURS = 48


@shared_task(name="mel.feedback.scan_due_survey_reminders")
def scan_due_survey_reminders() -> dict:
    """G16 — send the single pre-due reminder for SENT dispatches.

    Fires when ``due_at - now`` is below :data:`PRE_DUE_REMINDER_HOURS` but
    still positive (the survey hasn't actually lapsed yet) and the dispatch
    has not been reminded before. Idempotent via ``reminded_at IS NULL``.
    """
    from datetime import timedelta

    from apps.core.notifications.models import Notification
    from apps.core.notifications.services import send_notification
    from apps.mel.feedback.models import SurveyDispatch
    from apps.mel.feedback.services import survey_dispatch_take_url

    now = timezone.now()
    cutoff = now + timedelta(hours=PRE_DUE_REMINDER_HOURS)
    candidates = SurveyDispatch.objects.filter(
        status=SurveyDispatch.Status.SENT,
        reminded_at__isnull=True,
        due_at__isnull=False,
        due_at__gt=now,
        due_at__lte=cutoff,
    ).select_related("survey", "recipient")

    reminded = 0
    for dispatch in candidates:
        dispatch.status = SurveyDispatch.Status.REMINDED
        dispatch.reminded_at = now
        dispatch.save(update_fields=["status", "reminded_at"])
        reminded += 1
        if dispatch.recipient_id is not None:
            try:
                take_url = survey_dispatch_take_url(dispatch)
                message = (
                    f"Reminder: the survey ‘{dispatch.survey.title}’ closes "
                    f"{timezone.localtime(dispatch.due_at).strftime('%-d %b %H:%M')}."
                )
                send_notification(
                    dispatch.recipient,
                    message,
                    verb=Notification.Verb.MEL_FEEDBACK_REMINDER,
                    email_context={
                        "body": message,
                        "action_url": take_url,
                        "action_label": "Complete the survey",
                        "take_url": take_url,
                    },
                    data={"take_url": take_url},
                    action_url=take_url,
                )
            except Exception:  # pragma: no cover — never break the sweep
                logger.exception("survey pre-due reminder dispatch failed pk=%s", dispatch.pk)
    if reminded:
        logger.info("mel.survey.reminder.pre_due reminded=%s", reminded)
    return {"reminded": reminded}


@shared_task(name="mel.feedback.scan_survey_non_responses")
def scan_survey_non_responses() -> dict:
    """Move overdue SurveyDispatch rows through SENT → REMINDED → NON_RESPONDED.

    Uses the survey's ``response_window_days`` as the spacing between states.
    Returns a small summary dict for celery-results inspection.
    """
    from datetime import timedelta

    from apps.core.notifications.models import Notification
    from apps.core.notifications.services import send_notification
    from apps.mel.feedback.models import SurveyDispatch
    from apps.mel.feedback.services import survey_dispatch_take_url

    now = timezone.now()
    reminded = 0
    flagged = 0

    sent = SurveyDispatch.objects.filter(
        status=SurveyDispatch.Status.SENT,
        due_at__isnull=False,
        due_at__lt=now,
    ).select_related("survey", "recipient")
    for dispatch in sent:
        dispatch.status = SurveyDispatch.Status.REMINDED
        dispatch.reminded_at = now
        # Extend the window for the second sweep.
        dispatch.due_at = now + timedelta(days=int(dispatch.survey.response_window_days or 14))
        dispatch.save(update_fields=["status", "reminded_at", "due_at"])
        reminded += 1
        if dispatch.recipient_id is not None:
            try:
                take_url = survey_dispatch_take_url(dispatch)
                message = f"Reminder: please complete the survey ‘{dispatch.survey.title}’."
                send_notification(
                    dispatch.recipient,
                    message,
                    verb=Notification.Verb.MEL_FEEDBACK_REMINDER,
                    email_context={
                        "body": message,
                        "action_url": take_url,
                        "action_label": "Complete the survey",
                        "take_url": take_url,
                    },
                    data={"take_url": take_url},
                    action_url=take_url,
                )
            except Exception:  # pragma: no cover — notification failure must not stop the sweep
                logger.exception("survey reminder dispatch failed pk=%s", dispatch.pk)

    overdue_after_reminder = SurveyDispatch.objects.filter(
        status=SurveyDispatch.Status.REMINDED,
        due_at__isnull=False,
        due_at__lt=now,
    )
    for dispatch in overdue_after_reminder:
        dispatch.status = SurveyDispatch.Status.NON_RESPONDED
        dispatch.save(update_fields=["status"])
        flagged += 1
        # Cross-module: bump the participant timeline + SMEHubTrackingRecord
        # non_response counter when the recipient is an entrepreneur.
        try:
            user = dispatch.recipient
            if user is not None:
                from apps.mel.tracking.models import (
                    SMEHubTrackingRecord,
                    TrackingRecord as _TR,
                )
                from apps.mel.tracking.services import record_event

                record_event(
                    user=user,
                    source_module=_TR.Module.SMEHUB,
                    event_type="survey_non_response",
                    source_id=f"dispatch:{dispatch.pk}",
                    payload={"survey": dispatch.survey.slug},
                    label=f"No response: {dispatch.survey.title[:80]}",
                )
                # Reverse accessor on EntrepreneurProfile.user is
                # ``smehub_entrepreneur`` (onboarding/models.py:100); the old
                # ``entrepreneur_profile`` name never existed so the counter
                # was permanently 0 and the data-quality tile stayed empty (D-D6).
                profile = getattr(user, "smehub_entrepreneur", None)
                if profile is not None:
                    SMEHubTrackingRecord.objects.filter(entrepreneur=profile).update(
                        feedback_non_response_count=models.F("feedback_non_response_count") + 1
                    )
        except Exception:  # pragma: no cover - defensive
            logger.exception("non-response cross-module mirror failed pk=%s", dispatch.pk)

    if reminded or flagged:
        logger.info("mel.survey.scan reminded=%s flagged=%s", reminded, flagged)
    return {"reminded": reminded, "flagged": flagged}


@shared_task(name="mel.feedback.scan_post_project_tracers")
def scan_post_project_tracers() -> dict:
    """G6 — auto-dispatch tracers attached to closed log frames once the
    post-project window has elapsed.

    Idempotent: only TracerStudy rows with ``dispatched_at IS NULL`` and a
    ``dispatch_after_logframe_close`` log frame whose closure has aged past
    ``post_project_window_months`` qualify. The dispatch stamps
    ``dispatched_at`` so a re-run is a perfect no-op.

    "Dispatch" today means activating the study (``is_active = True``) and
    notifying the log frame owner so they can publish the share link via the
    existing tracer/feedback channels. A fully automated mailout to a
    pre-computed recipient list is intentionally out of scope — the
    M&EL officer remains the human-in-the-loop for sample selection.
    """
    from datetime import timedelta

    from apps.core.notifications.models import Notification
    from apps.core.notifications.services import send_notification
    from apps.mel.feedback.models import TracerStudy
    from apps.mel.indicators.models import LogFrameStatus

    now = timezone.now()
    candidates = (
        TracerStudy.objects.filter(
            dispatched_at__isnull=True,
            dispatch_after_logframe_close__isnull=False,
            dispatch_after_logframe_close__status=LogFrameStatus.CLOSED,
            dispatch_after_logframe_close__closed_at__isnull=False,
        )
        .select_related("dispatch_after_logframe_close__owner")
    )

    dispatched = 0
    for study in candidates:
        lf = study.dispatch_after_logframe_close
        # 30.4375 ≈ average days per month — close enough for a quarterly trigger.
        window_days = (lf.post_project_window_months or 3) * 30.4375
        if (now - lf.closed_at) < timedelta(days=window_days):
            continue
        TracerStudy.objects.filter(pk=study.pk, dispatched_at__isnull=True).update(
            dispatched_at=now,
            is_active=True,
        )
        dispatched += 1
        owner = getattr(lf, "owner", None)
        if owner is not None:
            try:
                send_notification(
                    owner,
                    f"Tracer study ‘{study.name}’ is now live for the closed "
                    f"programme ‘{lf.name}’. Share the survey link with respondents.",
                    verb=Notification.Verb.MEL_FEEDBACK_REMINDER,
                )
            except Exception:  # pragma: no cover — never break the sweep
                logger.exception("post-project tracer notification failed pk=%s", study.pk)

    if dispatched:
        logger.info("mel.tracer.post_project dispatched=%s", dispatched)
    return {"dispatched": dispatched}
