"""SME-Hub → M&EL signal receivers (Sub-Phase 6.6).

Translates every SP1–SP5 domain signal into a write on
:class:`SMEHubTrackingRecord` so M&EL has a longitudinal trail per
entrepreneur (FRSME-MEI001 → MEI007).

Each handler is wrapped with ``_safe_handler`` — failures are written to
:class:`SMEHubFeedHandlerLog` instead of propagating up the upstream
transaction. SP1, SP2, SP3, SP4, and SP5 services therefore never fail
because M&EL hit an unexpected payload (FRSME-MEI007).

Imported at AppConfig.ready() time via ``apps.mel.tracking.receivers``.
"""
from __future__ import annotations

import functools
import logging
from decimal import Decimal
from typing import Any, Callable

from django.dispatch import receiver
from django.utils import timezone

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _get_or_create_record(entrepreneur):
    """Lookup is always get_or_create — never assume the record exists."""
    if entrepreneur is None:
        return None
    from apps.mel.tracking.models import SMEHubTrackingRecord

    record, _ = SMEHubTrackingRecord.objects.get_or_create(entrepreneur=entrepreneur)
    return record


# Slice C reliability — when a feed handler fails for one of these CRITICAL
# signals, also fire an in-app alert to MEL officers so silent data gaps in
# the MEI feed are caught in minutes instead of days.
CRITICAL_FEEDS = {
    "smehub_baseline_initialised",         # MEI001 — root of the tracking record
    "smehub_connection_accepted",          # MEI004 — partnership feed
    "smehub_mou_formalised",               # MEI004 — formal partnership
    "smehub_funding_application_decided",  # MEI006 — funding decision feed
    "smehub_funding_secured",              # MEI006 — funding event feed
    "smehub_agreement_formalised",         # MEI004/006 — deal agreement
}


def _alert_mel_officers(signal_name: str, exc: BaseException) -> None:
    """Notify MEL officers (admins included) of a critical-feed failure."""
    try:
        from django.contrib.auth import get_user_model
        from apps.smehub._shared.notifications import Verb, bulk_notify_smehub

        User = get_user_model()
        recipients = list(
            User.objects.filter(
                role__in=("mel_officer", "system_admin", "admin", "sme_admin"),
                is_active=True,
            ).only("pk")
        )
        if not recipients:
            return
        bulk_notify_smehub(
            recipients,
            f"M&E feed handler failed for {signal_name} — {type(exc).__name__}",
            verb=getattr(Verb, "SMEHUB_FEED_HANDLER_FAILED", None) or "smehub.feed_handler_failed",
        )
    except Exception:  # pragma: no cover — best-effort alerting
        logger.exception("smehub-mel critical-feed alert failed for %s", signal_name)


def _log_failure(signal_name: str, exc: BaseException, *, entrepreneur=None, payload: dict | None = None) -> None:
    """Persist a SMEHubFeedHandlerLog row + log the traceback."""
    try:
        from apps.mel.tracking.models import SMEHubFeedHandlerLog

        SMEHubFeedHandlerLog.objects.create(
            signal_name=signal_name,
            payload=payload or {},
            error=f"{type(exc).__name__}: {exc}",
            entrepreneur=entrepreneur,
        )
    except Exception:  # pragma: no cover — last-resort guard
        logger.exception("smehub-mel feed-handler logging itself failed for %s", signal_name)
    logger.warning("smehub-mel signal handler failed: %s — %s", signal_name, exc)
    if signal_name in CRITICAL_FEEDS:
        _alert_mel_officers(signal_name, exc)


def _safe_handler(signal_name: str) -> Callable:
    """Decorator: never let a SME-Hub receiver bubble — log to FeedHandlerLog."""

    def wrap(fn):
        @functools.wraps(fn)
        def inner(*args, **kwargs):
            try:
                return fn(*args, **kwargs)
            except Exception as exc:  # noqa: BLE001
                # Best-effort entrepreneur extraction from kwargs for log row.
                entrepreneur = None
                for key in ("entrepreneur", "application", "member", "session", "connection",
                            "connection_request", "mou", "request", "score", "deal_room",
                            "agreement", "record", "entry", "response"):
                    obj = kwargs.get(key)
                    if obj is None:
                        continue
                    candidate = getattr(obj, "entrepreneur", None)
                    if candidate is not None:
                        entrepreneur = candidate
                        break
                _log_failure(signal_name, exc, entrepreneur=entrepreneur, payload={
                    k: getattr(v, "pk", str(v)) for k, v in kwargs.items() if k != "signal"
                })

        return inner

    return wrap


# ---------------------------------------------------------------------------
# SP1 onboarding — baseline_initialised is the ONLY entry that sets sp1_baseline_at
# ---------------------------------------------------------------------------


def _wire_onboarding() -> None:
    try:
        from apps.smehub.onboarding import signals as onb_signals
    except ImportError:
        return

    @receiver(onb_signals.smehub_baseline_initialised, dispatch_uid="mel.smehub.baseline_initialised")
    @_safe_handler("smehub_baseline_initialised")
    def on_baseline_initialised(sender, business=None, baseline=None, entrepreneur=None, **kwargs):
        if entrepreneur is None and business is not None:
            entrepreneur = business.entrepreneur
        record = _get_or_create_record(entrepreneur)
        if record is None:
            return
        update_fields: list[str] = []
        is_new_baseline = record.sp1_baseline_at is None
        if is_new_baseline:
            record.sp1_baseline_at = timezone.now()
            update_fields.append("sp1_baseline_at")
        if business is not None and record.business_id != getattr(business, "pk", None):
            record.business = business
            update_fields.append("business")
        if baseline is not None and record.baseline_snapshot_id != getattr(baseline, "pk", None):
            record.baseline_snapshot = baseline
            update_fields.append("baseline_snapshot")
        if update_fields:
            record.save(update_fields=update_fields + ["updated_at"])
        # Cross-module participant timeline — link the entrepreneur to a
        # Participant + ENTREPRENEUR persona and capture the SP1 baseline.
        try:
            user = getattr(entrepreneur, "user", None) if entrepreneur is not None else None
            if user is not None:
                from apps.mel.tracking.models import (
                    ParticipantPersona,
                    TrackingRecord as _TR,
                )
                from apps.mel.tracking.services import (
                    capture_baseline,
                    ensure_participant,
                    record_event,
                )

                participant = ensure_participant(user, persona=ParticipantPersona.ENTREPRENEUR)
                if participant is not None and participant.entrepreneur_profile_id != getattr(
                    entrepreneur, "pk", None
                ):
                    participant.entrepreneur_profile = entrepreneur
                    participant.save(update_fields=["entrepreneur_profile", "updated_at"])
                if is_new_baseline:
                    record_event(
                        user=user,
                        source_module=_TR.Module.SMEHUB,
                        event_type="smehub_baseline_initialised",
                        source_id=record.pk,
                        payload={
                            "business_id": getattr(business, "pk", None),
                            "baseline_id": getattr(baseline, "pk", None),
                        },
                        persona=ParticipantPersona.ENTREPRENEUR,
                        label="SME-Hub baseline initialised",
                    )
                    if baseline is not None:
                        for dim_field, dim_code in (
                            ("annual_revenue", "business_revenue"),
                            ("fte_count", "employment_fte"),
                            ("pte_count", "employment_pte"),
                        ):
                            value = getattr(baseline, dim_field, None)
                            if value is None:
                                continue
                            capture_baseline(
                                user=user,
                                source_module=_TR.Module.SMEHUB,
                                dimension=dim_code,
                                value_numeric=value,
                                note=f"SP1 baseline ({dim_field})",
                            )
        except Exception:  # pragma: no cover — defensive
            logger.exception("mel.timeline baseline mirror failed entrepreneur=%s", getattr(entrepreneur, "pk", None))
        # FRSME-OBR037 — notify M&E officers when a brand-new tracking record
        # transitions out of the empty baseline state. Real-time visibility
        # of new SME-Hub entrants on the M&EL dashboard.
        if is_new_baseline:
            _notify_mel_officers_new_tracking_record(record, entrepreneur, business)


def _notify_mel_officers_new_tracking_record(record, entrepreneur, business) -> None:
    """Dispatch SMEHUB_NEW_TRACKING_RECORD to every active M&E officer
    + SME admin. Best-effort: failures are swallowed (the upstream signal
    must succeed even if notifications hit a snag)."""
    try:
        from django.contrib.auth import get_user_model

        from apps.core.permissions.roles import UserRole
        from apps.smehub._shared.notifications import bulk_notify_smehub, Verb

        User = get_user_model()
        recipients = list(
            User.objects.filter(
                is_active=True,
                role__in=[
                    UserRole.MEL_OFFICER,
                    UserRole.SME_ADMIN,
                    UserRole.ADMIN,
                    UserRole.SYSTEM_ADMIN,
                ],
            )
        )
        if not recipients:
            return
        biz_name = getattr(business, "name", "") or "(no business name)"
        ent_name = ""
        if entrepreneur is not None and getattr(entrepreneur, "user", None):
            ent_name = entrepreneur.user.get_full_name() or entrepreneur.user.email
        from django.urls import reverse

        try:
            action_url = reverse(
                "mel_tracking:smehub_tracking_detail", kwargs={"pk": record.pk}
            )
        except Exception:  # pragma: no cover — URL resolution guard
            action_url = reverse("mel_tracking:smehub_portfolio")
        bulk_notify_smehub(
            recipients,
            message=(
                f"New SME-Hub entrant in M&E portfolio: {ent_name} "
                f"({biz_name}). Tracking record initialised."
            ),
            verb=Verb.SMEHUB_NEW_TRACKING_RECORD,
            action_url=action_url,
            email_context={
                "entrepreneur_name": ent_name,
                "business_name": biz_name,
                "tracking_record_id": record.pk,
            },
        )
    except Exception:  # pragma: no cover - notification failure must not propagate
        logger.exception("MEL officer notification failed for new tracking record")


# ---------------------------------------------------------------------------
# SP2 incubation
# ---------------------------------------------------------------------------


def _wire_incubation() -> None:
    try:
        from apps.smehub.incubation import signals as inc_signals
    except ImportError:
        return

    @receiver(inc_signals.smehub_application_submitted, dispatch_uid="mel.smehub.application_submitted")
    @_safe_handler("smehub_application_submitted")
    def on_application_submitted(sender, application=None, **kwargs):
        if application is None:
            return
        record = _get_or_create_record(application.entrepreneur)
        if record is not None:
            record.append_milestone(
                bucket="sp2_milestones",
                key="application_submitted",
                payload={
                    "id": application.pk,
                    "programme_id": application.programme_id,
                    "business_id": application.business_id,
                },
            )

    @receiver(inc_signals.smehub_application_decided, dispatch_uid="mel.smehub.application_decided")
    @_safe_handler("smehub_application_decided")
    def on_application_decided(sender, application=None, by_user=None, accepted=None, score=None, **kwargs):
        if application is None:
            return
        record = _get_or_create_record(application.entrepreneur)
        if record is not None:
            record.append_milestone(
                bucket="sp2_milestones",
                key="application_decided",
                payload={
                    "id": application.pk,
                    "accepted": accepted,
                    "score": str(score) if score is not None else None,
                    "by_user_id": getattr(by_user, "pk", None),
                },
            )

    @receiver(inc_signals.smehub_cohort_confirmed, dispatch_uid="mel.smehub.cohort_confirmed")
    @_safe_handler("smehub_cohort_confirmed")
    def on_cohort_confirmed(sender, cohort=None, members=None, **kwargs):
        if cohort is None:
            return
        for member in members or []:
            record = _get_or_create_record(member.entrepreneur)
            if record is not None:
                record.append_milestone(
                    bucket="sp2_milestones",
                    key="cohort_selected",
                    payload={
                        "id": member.pk,
                        "cohort_id": cohort.pk,
                        "programme_id": cohort.programme_id,
                    },
                )

    @receiver(inc_signals.smehub_cohort_member_completed, dispatch_uid="mel.smehub.member_completed")
    @_safe_handler("smehub_cohort_member_completed")
    def on_cohort_member_completed(sender, member=None, manual=False, **kwargs):
        if member is None:
            return
        record = _get_or_create_record(member.entrepreneur)
        if record is not None:
            record.append_milestone(
                bucket="sp2_milestones",
                key="programme_complete",
                payload={
                    "id": member.pk,
                    "cohort_id": member.cohort_id,
                    "manual": bool(manual),
                },
            )
        # Beneficiary feedback dispatch (FRSME-MEI017).
        try:
            from apps.mel.tracking.smehub_feedback import dispatch_post_incubation_survey

            dispatch_post_incubation_survey(record=record, member=member)
        except Exception as exc:  # noqa: BLE001
            logger.warning("post-incubation survey dispatch failed: %s", exc)
        # Configurable officer-authored Survey dispatch (FRSME-MEI017).
        try:
            from apps.mel.feedback.models import SurveyTrigger
            from apps.mel.tracking.smehub_feedback import dispatch_triggered_surveys

            dispatch_triggered_surveys(
                trigger=SurveyTrigger.POST_INCUBATION,
                record=record,
                trigger_event=f"cohort_member_completed:{member.pk}",
            )
        except Exception as exc:  # noqa: BLE001
            logger.warning("post-incubation triggered-survey dispatch failed: %s", exc)

    @receiver(inc_signals.smehub_session_completed, dispatch_uid="mel.smehub.session_completed")
    @_safe_handler("smehub_session_completed")
    def on_session_completed(sender, session=None, **kwargs):
        if session is None:
            return
        # Session belongs to a cohort_member; entrepreneur is on the member.
        member = getattr(session.assignment, "cohort_member", None) if session.assignment_id else None
        entrepreneur = member.entrepreneur if member is not None else None
        record = _get_or_create_record(entrepreneur)
        if record is not None:
            record.append_milestone(
                bucket="sp2_milestones",
                key="session_completed",
                payload={"id": session.pk, "member_id": getattr(member, "pk", None)},
            )

    @receiver(inc_signals.smehub_progress_milestone, dispatch_uid="mel.smehub.progress_milestone")
    @_safe_handler("smehub_progress_milestone")
    def on_progress_milestone(sender, member=None, milestone=None, payload=None, **kwargs):
        if member is None:
            return
        record = _get_or_create_record(member.entrepreneur)
        if record is not None:
            entry_payload = {"id": member.pk, "milestone": milestone}
            if isinstance(payload, dict):
                entry_payload.update(payload)
            record.append_milestone(
                bucket="sp2_milestones",
                key="course_completed",
                payload=entry_payload,
            )

    @receiver(inc_signals.smehub_certificate_issued, dispatch_uid="mel.smehub.certificate_issued")
    @_safe_handler("smehub_certificate_issued")
    def on_certificate_issued(sender, certificate=None, member=None, **kwargs):
        if certificate is None:
            return
        target_member = member or getattr(certificate, "cohort_member", None)
        entrepreneur = getattr(target_member, "entrepreneur", None)
        record = _get_or_create_record(entrepreneur)
        if record is not None:
            record.append_milestone(
                bucket="sp2_milestones",
                key="certificate_issued",
                payload={
                    "id": certificate.pk,
                    "member_id": getattr(target_member, "pk", None),
                },
            )


# ---------------------------------------------------------------------------
# SP3 linkage
# ---------------------------------------------------------------------------


def _wire_linkage() -> None:
    try:
        from apps.smehub.linkage import signals as lnk_signals
    except ImportError:
        return

    @receiver(lnk_signals.smehub_connection_accepted, dispatch_uid="mel.smehub.connection_accepted")
    @_safe_handler("smehub_connection_accepted")
    def on_connection_accepted(sender, connection_request=None, connection=None, by_user=None, **kwargs):
        target = connection or connection_request
        if target is None:
            return
        record = _get_or_create_record(target.entrepreneur)
        if record is not None:
            record.append_milestone(
                bucket="sp3_partnerships",
                key="connection_accepted",
                payload={
                    "id": getattr(connection, "pk", None),
                    "request_id": getattr(connection_request, "pk", None),
                    "by_user_id": getattr(by_user, "pk", None),
                },
            )
        # Beneficiary feedback dispatch (FRSME-MEI018).
        try:
            from apps.mel.tracking.smehub_feedback import dispatch_post_connection_survey

            dispatch_post_connection_survey(
                record=record, connection=connection, request=connection_request,
            )
        except Exception as exc:  # noqa: BLE001
            logger.warning("post-connection survey dispatch failed: %s", exc)
        # Configurable officer-authored Survey dispatch (FRSME-MEI018).
        try:
            from apps.mel.feedback.models import SurveyTrigger
            from apps.mel.tracking.smehub_feedback import dispatch_triggered_surveys

            _conn_target = connection or connection_request
            if _conn_target is not None:
                dispatch_triggered_surveys(
                    trigger=SurveyTrigger.POST_CONNECTION,
                    record=record,
                    trigger_event=f"connection_accepted:{_conn_target.pk}",
                )
        except Exception as exc:  # noqa: BLE001
            logger.warning("post-connection triggered-survey dispatch failed: %s", exc)

    @receiver(lnk_signals.smehub_mou_formalised, dispatch_uid="mel.smehub.mou_formalised")
    @_safe_handler("smehub_mou_formalised")
    def on_mou_formalised(sender, mou=None, by_user=None, **kwargs):
        if mou is None:
            return
        entrepreneur = getattr(getattr(mou, "connection", None), "entrepreneur", None)
        record = _get_or_create_record(entrepreneur)
        if record is not None:
            record.append_milestone(
                bucket="sp3_partnerships",
                key="mou_formalised",
                payload={"id": mou.pk, "by_user_id": getattr(by_user, "pk", None)},
            )

    @receiver(lnk_signals.smehub_advisory_completed, dispatch_uid="mel.smehub.advisory_completed")
    @_safe_handler("smehub_advisory_completed")
    def on_advisory_completed(sender, session=None, **kwargs):
        if session is None:
            return
        record = _get_or_create_record(session.entrepreneur)
        if record is not None:
            record.append_milestone(
                bucket="sp3_partnerships",
                key="advisory_completed",
                payload={
                    "id": session.pk,
                    "provider_id": getattr(session, "service_provider_id", None),
                },
            )


# ---------------------------------------------------------------------------
# SP3b marketplace — demand_responded counts as a partnership signal.
# ---------------------------------------------------------------------------


def _wire_marketplace() -> None:
    try:
        from apps.smehub.marketplace import signals as mkt_signals
    except ImportError:
        return

    @receiver(mkt_signals.smehub_demand_response_submitted, dispatch_uid="mel.smehub.demand_response_submitted")
    @_safe_handler("smehub_demand_response_submitted")
    def on_demand_response_submitted(sender, response=None, **kwargs):
        # FRSME-MPL020 — the SRS records a market-linkage indicator each time a
        # demand listing is *responded to* (not only when awarded). The AWARDED
        # decision below additionally records the confirmed-partnership outcome.
        if response is None:
            return
        record = _get_or_create_record(response.entrepreneur)
        if record is not None:
            record.append_milestone(
                bucket="sp3_partnerships",
                key="demand_response_submitted",
                payload={
                    "id": response.pk,
                    "demand_listing_id": response.demand_listing_id,
                },
            )

    @receiver(mkt_signals.smehub_demand_response_decided, dispatch_uid="mel.smehub.demand_response_decided")
    @_safe_handler("smehub_demand_response_decided")
    def on_demand_response_decided(sender, response=None, by_user=None, outcome=None, **kwargs):
        if response is None:
            return
        # Only AWARDED responses count as a confirmed partnership for M&EL.
        from apps.smehub.marketplace.models import DemandResponse

        if outcome != DemandResponse.Status.AWARDED:
            return
        record = _get_or_create_record(response.entrepreneur)
        if record is not None:
            record.append_milestone(
                bucket="sp3_partnerships",
                key="demand_responded",
                payload={
                    "id": response.pk,
                    "demand_listing_id": response.demand_listing_id,
                    "outcome": outcome,
                    "by_user_id": getattr(by_user, "pk", None),
                },
            )


# ---------------------------------------------------------------------------
# SP4 showcasing
# ---------------------------------------------------------------------------


def _wire_showcasing() -> None:
    try:
        from apps.smehub.showcasing import signals as show_signals
    except ImportError:
        return

    @receiver(show_signals.smehub_showcase_participant_confirmed,
              dispatch_uid="mel.smehub.showcase_participant_confirmed")
    @_safe_handler("smehub_showcase_participant_confirmed")
    def on_showcase_participant_confirmed(sender, application=None, by_user=None, **kwargs):
        if application is None:
            return
        record = _get_or_create_record(application.entrepreneur)
        if record is not None:
            record.append_milestone(
                bucket="sp4_commercialisation",
                key="showcase_participated",
                payload={
                    "id": application.pk,
                    "target_type": str(application.target_content_type) if application.target_content_type_id else None,
                    "target_id": application.target_object_id,
                    "by_user_id": getattr(by_user, "pk", None),
                },
            )

    @receiver(show_signals.smehub_pitch_scored, dispatch_uid="mel.smehub.pitch_scored")
    @_safe_handler("smehub_pitch_scored")
    def on_pitch_scored(sender, score=None, **kwargs):
        if score is None:
            return
        application = getattr(score, "application", None)
        if application is None:
            return
        # Event composites use section=None; challenge per-section scores fire repeatedly,
        # so we only persist the event-composite signal here. Challenges aggregate via
        # `smehub_challenge_announced` (and are already represented in the milestone
        # trail via `showcase_participated`).
        if score.section_id is not None:
            return
        record = _get_or_create_record(application.entrepreneur)
        if record is not None:
            record.append_milestone(
                bucket="sp4_commercialisation",
                key="challenge_ranked",
                payload={
                    "id": score.pk,
                    "application_id": application.pk,
                    "score": str(score.score) if score.score is not None else None,
                    "judge_id": score.judge_id,
                },
            )

    @receiver(show_signals.smehub_challenge_announced, dispatch_uid="mel.smehub.challenge_announced")
    @_safe_handler("smehub_challenge_announced")
    def on_challenge_announced(sender, challenge=None, winners=None, **kwargs):
        # FRSME-ISD020 — record the "innovation challenge entry ranked/awarded"
        # commercialisation indicator for every announced winner.
        for application in winners or []:
            record = _get_or_create_record(application.entrepreneur)
            if record is not None:
                record.append_milestone(
                    bucket="sp4_commercialisation",
                    key="challenge_awarded",
                    payload={
                        "id": application.pk,
                        "challenge_id": getattr(challenge, "pk", None),
                        "challenge_title": getattr(challenge, "title", ""),
                    },
                )

    @receiver(show_signals.smehub_deal_room_opened, dispatch_uid="mel.smehub.deal_room_opened")
    @_safe_handler("smehub_deal_room_opened")
    def on_deal_room_opened(sender, deal_room=None, by_user=None, **kwargs):
        if deal_room is None:
            return
        record = _get_or_create_record(deal_room.entrepreneur)
        if record is not None:
            record.append_milestone(
                bucket="sp4_commercialisation",
                key="deal_room_opened",
                payload={
                    "id": deal_room.pk,
                    "business_id": deal_room.business_id,
                    "by_user_id": getattr(by_user, "pk", None),
                },
            )

    @receiver(show_signals.smehub_agreement_formalised, dispatch_uid="mel.smehub.agreement_formalised")
    @_safe_handler("smehub_agreement_formalised")
    def on_agreement_formalised(sender, deal_room=None, agreement=None, by_user=None, **kwargs):
        if deal_room is None:
            return
        record = _get_or_create_record(deal_room.entrepreneur)
        if record is not None:
            record.append_milestone(
                bucket="sp4_commercialisation",
                key="agreement_formalised",
                payload={
                    "id": deal_room.pk,
                    "agreement_id": getattr(agreement, "pk", None),
                    "by_user_id": getattr(by_user, "pk", None),
                },
            )
        # Beneficiary feedback dispatch (FRSME-MEI019).
        try:
            from apps.mel.tracking.smehub_feedback import dispatch_post_deal_survey

            dispatch_post_deal_survey(record=record, deal_room=deal_room, agreement=agreement)
        except Exception as exc:  # noqa: BLE001
            logger.warning("post-deal survey dispatch failed: %s", exc)
        # Configurable officer-authored Survey dispatch (FRSME-MEI019).
        try:
            from apps.mel.feedback.models import SurveyTrigger
            from apps.mel.tracking.smehub_feedback import dispatch_triggered_surveys

            dispatch_triggered_surveys(
                trigger=SurveyTrigger.POST_DEAL,
                record=record,
                trigger_event=f"agreement_formalised:{deal_room.pk}",
            )
        except Exception as exc:  # noqa: BLE001
            logger.warning("post-deal triggered-survey dispatch failed: %s", exc)


# ---------------------------------------------------------------------------
# SP5 investment
# ---------------------------------------------------------------------------


def _bump_funding_total(record, *, amount) -> None:
    """Add `amount` to the running funding_secured_total."""
    if amount is None or record is None:
        return
    try:
        delta = Decimal(str(amount))
    except Exception:  # noqa: BLE001
        return
    if delta <= 0:
        return
    record.funding_secured_total = (record.funding_secured_total or Decimal("0")) + delta
    record.save(update_fields=["funding_secured_total", "updated_at"])


def _wire_investment() -> None:
    try:
        from apps.smehub.investment import signals as inv_signals
    except ImportError:
        return

    @receiver(inv_signals.smehub_funding_application_submitted,
              dispatch_uid="mel.smehub.funding_application_submitted")
    @_safe_handler("smehub_funding_application_submitted")
    def on_funding_application_submitted(sender, application=None, **kwargs):
        if application is None:
            return
        record = _get_or_create_record(application.entrepreneur)
        if record is not None:
            record.append_milestone(
                bucket="sp5_investment",
                key="funding_application_submitted",
                payload={
                    "id": application.pk,
                    "funding_call_id": application.funding_call_id,
                    "amount": str(application.funding_request_amount)
                    if application.funding_request_amount is not None
                    else None,
                },
            )

    @receiver(inv_signals.smehub_funding_application_decided,
              dispatch_uid="mel.smehub.funding_application_decided")
    @_safe_handler("smehub_funding_application_decided")
    def on_funding_application_decided(sender, application=None, action=None, by_user=None, **kwargs):
        if application is None:
            return
        record = _get_or_create_record(application.entrepreneur)
        if record is not None:
            record.append_milestone(
                bucket="sp5_investment",
                key="funding_outcome",
                payload={
                    "id": application.pk,
                    "action": action,
                    "by_user_id": getattr(by_user, "pk", None),
                },
            )

    @receiver(inv_signals.smehub_investor_request_accepted,
              dispatch_uid="mel.smehub.investor_request_accepted")
    @_safe_handler("smehub_investor_request_accepted")
    def on_investor_request_accepted(sender, request=None, deal_room=None, by_user=None, **kwargs):
        if request is None:
            return
        record = _get_or_create_record(request.entrepreneur)
        if record is not None:
            record.append_milestone(
                bucket="sp5_investment",
                key="investor_connection_accepted",
                payload={
                    "id": request.pk,
                    "investor_id": request.investor_id,
                    "deal_room_id": getattr(deal_room, "pk", None),
                    "by_user_id": getattr(by_user, "pk", None),
                },
            )

    @receiver(inv_signals.smehub_funding_secured, dispatch_uid="mel.smehub.funding_secured")
    @_safe_handler("smehub_funding_secured")
    def on_funding_secured(sender, entrepreneur=None, business=None, amount=None, source=None, **kwargs):
        if entrepreneur is None:
            return
        record = _get_or_create_record(entrepreneur)
        if record is not None:
            record.append_milestone(
                bucket="sp5_investment",
                key="funding_secured",
                payload={
                    "business_id": getattr(business, "pk", None),
                    "amount": str(amount) if amount is not None else None,
                    "source": source,
                },
            )
            _bump_funding_total(record, amount=amount)

    @receiver(inv_signals.smehub_manual_funding_recorded,
              dispatch_uid="mel.smehub.manual_funding_recorded")
    @_safe_handler("smehub_manual_funding_recorded")
    def on_manual_funding_recorded(sender, record=None, by_user=None, **kwargs):
        if record is None:
            return
        tracking = _get_or_create_record(record.entrepreneur)
        if tracking is not None:
            tracking.append_milestone(
                bucket="sp5_investment",
                key="funding_secured",
                payload={
                    "manual_record_id": record.pk,
                    "business_id": record.business_id,
                    "amount": str(record.amount) if record.amount is not None else None,
                    "source": "manual",
                    "by_user_id": getattr(by_user, "pk", None),
                },
            )
            # Funding-secured signal also fires from the record_manual_funding service,
            # which bumps the running total — we do NOT bump it again here.


# ---------------------------------------------------------------------------
# Public entrypoint — invoked by apps.mel.tracking.receivers
# ---------------------------------------------------------------------------


def _wire_feedback_submission() -> None:
    """FRSME-MEI018 — link inbound FeedbackSubmissions to the beneficiary survey
    dispatch so non-response counters and completion rates stay accurate."""
    from django.db.models.signals import post_save

    from apps.mel.feedback.models import FeedbackSubmission
    from apps.mel.tracking.smehub_feedback import mark_completed_for_submission

    @receiver(post_save, sender=FeedbackSubmission, dispatch_uid="mel.smehub.feedback_completed")
    @_safe_handler("mel_feedback_submission_post_save")
    def on_feedback_submission_saved(sender, instance, created, **kwargs):
        if not created:
            return
        mark_completed_for_submission(instance)


def _wire_innovation_lifecycle() -> None:
    """Phase 6.1 — innovation onboarding/vetting lifecycle events."""
    try:
        from apps.smehub.onboarding import signals as onb_signals
    except ImportError:
        return

    @receiver(onb_signals.smehub_innovation_approved, dispatch_uid="mel.smehub.innovation_approved")
    @_safe_handler("smehub_innovation_approved")
    def on_innovation_approved(sender, innovation=None, by_user=None, **kwargs):
        if innovation is None:
            return
        _record_innovation_event(innovation, event_type="smehub_innovation_approved", by_user=by_user)

    @receiver(onb_signals.smehub_innovation_published, dispatch_uid="mel.smehub.innovation_published")
    @_safe_handler("smehub_innovation_published")
    def on_innovation_published(sender, innovation=None, by_user=None, **kwargs):
        if innovation is None:
            return
        _record_innovation_event(innovation, event_type="smehub_innovation_published", by_user=by_user)


def _record_innovation_event(innovation, *, event_type: str, by_user=None) -> None:
    from apps.mel.tracking.models import ParticipantPersona, TrackingRecord as _TR
    from apps.mel.tracking.services import record_event

    business = getattr(innovation, "business", None)
    entrepreneur = getattr(business, "entrepreneur", None) if business is not None else None
    user = getattr(entrepreneur, "user", None) if entrepreneur is not None else None
    if user is None:
        return
    payload = {
        "innovation_id": getattr(innovation, "innovation_id", ""),
        "title": getattr(innovation, "title", ""),
        "sector": getattr(innovation, "sector", ""),
        "stage": getattr(innovation, "stage", ""),
        "business_id": getattr(business, "business_id", ""),
        "by_user_id": getattr(by_user, "pk", None),
    }
    record_event(
        user=user,
        source_module=_TR.Module.SMEHUB,
        event_type=event_type,
        source_id=innovation.pk,
        payload=payload,
        persona=ParticipantPersona.ENTREPRENEUR,
        label=innovation.title or "innovation",
    )


def _wire_marketplace_outcomes() -> None:
    """Phase 6.3 — market validation + sales evidence into MEL timeline."""
    try:
        from apps.smehub.marketplace import signals as mkt_signals
    except ImportError:
        return

    @receiver(
        mkt_signals.smehub_market_validation_recorded,
        dispatch_uid="mel.smehub.market_validation_recorded",
    )
    @_safe_handler("smehub_market_validation_recorded")
    def on_market_validation_recorded(sender, validation=None, by_user=None, **kwargs):
        if validation is None:
            return
        _record_marketplace_event(
            owner_user=_owner_user_for_business(validation.business),
            event_type="smehub_market_validation_recorded",
            source_id=validation.pk,
            payload={
                "method": validation.method,
                "average_score": str(validation.average_score),
                "has_strong_signal": validation.has_strong_signal,
                "wtp_amount": str(validation.willingness_to_pay_amount or ""),
                "currency": validation.currency,
                "product_listing_id": validation.product_listing_id,
            },
            label=f"Validation ({validation.get_method_display()})",
        )

    @receiver(mkt_signals.smehub_sale_recorded, dispatch_uid="mel.smehub.sale_recorded")
    @_safe_handler("smehub_sale_recorded")
    def on_sale_recorded(sender, sale=None, by_user=None, **kwargs):
        if sale is None:
            return
        _record_marketplace_event(
            owner_user=sale.entrepreneur,
            event_type="smehub_sale_recorded",
            source_id=sale.pk,
            payload=_sale_payload(sale),
            label=f"Sale {sale.gross_amount} {sale.currency}",
        )

    @receiver(mkt_signals.smehub_sale_verified, dispatch_uid="mel.smehub.sale_verified")
    @_safe_handler("smehub_sale_verified")
    def on_sale_verified(sender, sale=None, by_user=None, **kwargs):
        if sale is None:
            return
        _record_marketplace_event(
            owner_user=sale.entrepreneur,
            event_type="smehub_sale_verified",
            source_id=sale.pk,
            payload=_sale_payload(sale),
            label=f"Sale verified {sale.gross_amount} {sale.currency}",
        )


def _owner_user_for_business(business):
    if business is None:
        return None
    entrepreneur = getattr(business, "entrepreneur", None)
    return getattr(entrepreneur, "user", None) if entrepreneur is not None else None


def _sale_payload(sale) -> dict:
    return {
        "channel": sale.channel,
        "gross_amount": str(sale.gross_amount),
        "currency": sale.currency,
        "product_listing_id": sale.product_listing_id,
        "demand_response_id": sale.demand_response_id,
        "buyer_entry_id": sale.buyer_entry_id,
        "sale_date": sale.sale_date.isoformat() if sale.sale_date else None,
    }


def _record_marketplace_event(*, owner_user, event_type: str, source_id, payload: dict, label: str) -> None:
    from apps.mel.tracking.models import ParticipantPersona, TrackingRecord as _TR
    from apps.mel.tracking.services import record_event

    if owner_user is None:
        return
    record_event(
        user=owner_user,
        source_module=_TR.Module.SMEHUB,
        event_type=event_type,
        source_id=source_id,
        payload=payload,
        persona=ParticipantPersona.ENTREPRENEUR,
        label=label,
    )


def wire_smehub_receivers() -> None:
    _wire_onboarding()
    _wire_incubation()
    _wire_linkage()
    _wire_marketplace()
    _wire_showcasing()
    _wire_investment()
    _wire_feedback_submission()
    _wire_innovation_lifecycle()
    _wire_marketplace_outcomes()
