"""Onboarding services — Subprocess SP1.

Each function corresponds to one or more PRD functional requirements
(FRSME-OBR001 → OBR037).  Side-effects (notification dispatch, audit logs,
M&E baseline emission) happen here, not on view layer.
"""
from __future__ import annotations

from datetime import timedelta
from typing import Any, Iterable

from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import make_password
from django.db import transaction
from django.urls import NoReverseMatch, reverse
from django.utils import timezone

from apps.core.notifications.models import Notification
from apps.core.permissions.roles import UserRole
from apps.smehub._shared.audit import AuditLog, _audit
from apps.smehub._shared.notifications import bulk_notify_smehub, notify_smehub
from apps.smehub.onboarding import signals as smehub_signals
from apps.smehub.onboarding.models import (
    AIHAffiliation,
    Business,
    BusinessBaseline,
    BuyerProfile,
    EntrepreneurProfile,
    EvaluatorAssignment,
    Innovation,
    InvestorProfile,
    MentorProfile,
    PartnerProfile,
    RegistrationRecord,
    ServiceProviderProfile,
    VettingRecord,
    VettingScore,
)

User = get_user_model()

def _verification_token_ttl() -> timedelta:
    """FRSME-OBR007/008 — verification link lifetime.

    Configurable via Django settings ``SMEHUB_VERIFICATION_TOKEN_TTL_HOURS``;
    default 48h matches the original hard-coded value flagged by the audit.
    Read fresh on each call so test overrides via ``override_settings`` work.
    """
    hours = int(getattr(settings, "SMEHUB_VERIFICATION_TOKEN_TTL_HOURS", 48))
    return timedelta(hours=hours)


VERIFICATION_TOKEN_TTL = _verification_token_ttl()  # legacy import; prefer the function

ROLE_TO_USERROLE = {
    RegistrationRecord.Role.ENTREPRENEUR: UserRole.ENTREPRENEUR,
    RegistrationRecord.Role.MENTOR: UserRole.MENTOR,
    RegistrationRecord.Role.INVESTOR: UserRole.INVESTOR,
    RegistrationRecord.Role.SERVICE_PROVIDER: UserRole.SERVICE_PROVIDER,
    RegistrationRecord.Role.PARTNER: UserRole.PARTNER,
    RegistrationRecord.Role.BUYER: UserRole.BUYER,
}


def _absolute_url(path: str) -> str:
    base = getattr(settings, "PUBLIC_APP_BASE_URL", "").rstrip("/")
    return f"{base}{path}" if base else path


# ---------------------------------------------------------------------------
# Registration  (FRSME-OBR001 → OBR010)
# ---------------------------------------------------------------------------

class DuplicateEmailError(Exception):
    """Raised when registration email collides with an active account."""


@transaction.atomic
def register_user(
    *,
    full_name: str,
    email: str,
    password: str,
    country: str,
    organisation: str,
    phone: str,
    role: str,
) -> RegistrationRecord:
    """Persist registration, dispatch verification email.

    PRD FRSME-OBR002 (immediate persistence), OBR004 (duplicate email block),
    OBR005 mandatory fields are enforced upstream by the form.
    """
    email_norm = email.strip().lower()
    if User.objects.filter(email__iexact=email_norm, is_active=True).exists():
        raise DuplicateEmailError(email_norm)

    record = RegistrationRecord.objects.create(
        full_name=full_name,
        email=email_norm,
        country=country,
        organisation=organisation,
        phone=phone,
        role=role,
        password_hash=make_password(password),
        token=RegistrationRecord.fresh_token(),
        token_expires_at=timezone.now() + _verification_token_ttl(),
    )
    smehub_signals.smehub_user_registered.send(
        sender=RegistrationRecord,
        registration=record,
        email=email_norm,
        role=role,
    )
    _dispatch_verification_email(record)
    return record


def resend_verification(*, email: str) -> RegistrationRecord | None:
    """Re-issue verification token without re-entry (FRSME-OBR009)."""
    record = (
        RegistrationRecord.objects.filter(email__iexact=email, consumed_at__isnull=True)
        .order_by("-created_at")
        .first()
    )
    if record is None:
        return None
    record.token = RegistrationRecord.fresh_token()
    record.token_expires_at = timezone.now() + _verification_token_ttl()
    record.save(update_fields=["token", "token_expires_at", "updated_at"])
    _dispatch_verification_email(record)
    return record


def _dispatch_verification_email(record: RegistrationRecord) -> None:
    try:
        path = reverse("smehub_onboarding:verify_email", kwargs={"token": record.token})
    except NoReverseMatch:
        path = f"/smehub/verify-email/{record.token}/"
    link = _absolute_url(path)
    notify_smehub(
        record_recipient_proxy(record),
        message=(
            f"Welcome to the RUFORUM SME-Hub. Click this link to confirm your "
            f"email and finish registering: {link}"
        ),
        verb=Notification.Verb.SMEHUB_REGISTRATION_VERIFY_EMAIL,
        action_url=path,
        target=record,
        email_context={
            "subject": "Confirm your RUFORUM SME-Hub registration",
            "verification_link": link,
            "full_name": record.full_name,
            "expires_at": record.token_expires_at,
        },
    )


def record_recipient_proxy(record: RegistrationRecord):
    """The Notification model requires a User FK.  When verifying, the user
    does not exist yet, so dispatch is decoupled — for now we use a transient
    inactive User row keyed off the registration email so the existing
    notification infra can deliver an email.  The user is reconciled (or
    deleted) when verification completes."""
    user, created = User.objects.get_or_create(
        email=record.email,
        defaults={
            "is_active": False,
            "first_name": record.full_name.split(" ")[0],
            "last_name": " ".join(record.full_name.split(" ")[1:]),
            "phone": record.phone,
            "role": ROLE_TO_USERROLE[record.role],
        },
    )
    if created:
        record.created_user = user
        record.save(update_fields=["created_user", "updated_at"])
    return user


class TokenInvalidError(Exception):
    """Verification token unknown, expired, or already consumed."""


@transaction.atomic
def verify_email(*, token: str) -> User:
    """Activate the user account, attach role, return the user.

    PRD FRSME-OBR007/OBR008.  Honours the password originally chosen by the
    applicant (stored hashed in `RegistrationRecord.password_hash`).
    """
    record = (
        RegistrationRecord.objects.select_for_update()
        .filter(token=token)
        .first()
    )
    if record is None or record.consumed_at is not None:
        raise TokenInvalidError("token_unknown_or_used")
    if record.token_expires_at < timezone.now():
        raise TokenInvalidError("token_expired")

    user = record.created_user
    if user is None:
        # Defensive: shouldn't happen because record_recipient_proxy creates one.
        user = User.objects.filter(email=record.email).first()
    if user is None:
        user = User.objects.create(
            email=record.email,
            first_name=record.full_name.split(" ")[0],
            last_name=" ".join(record.full_name.split(" ")[1:]),
            phone=record.phone,
        )
    user.is_active = True
    user.email_verified = True
    user.password = record.password_hash
    user.role = ROLE_TO_USERROLE[record.role]
    user.save(
        update_fields=[
            "is_active", "email_verified", "password", "role", "first_name",
            "last_name", "phone",
        ],
    )
    record.consumed_at = timezone.now()
    record.created_user = user
    record.save(update_fields=["consumed_at", "created_user", "updated_at"])

    _ensure_role_profile(user, record.role)
    smehub_signals.smehub_email_verified.send(
        sender=RegistrationRecord, user=user, registration=record,
    )
    _audit(user, AuditLog.Action.UPDATE, user, metadata={"event": "smehub_email_verified"})
    return user


def _ensure_role_profile(user: User, role: str):
    """Create the role-specific profile shell on first verification."""
    common = {
        "phone": getattr(user, "phone", ""),
    }
    if role == RegistrationRecord.Role.ENTREPRENEUR:
        EntrepreneurProfile.objects.get_or_create(user=user, defaults=common)
    elif role == RegistrationRecord.Role.MENTOR:
        MentorProfile.objects.get_or_create(user=user, defaults=common)
    elif role == RegistrationRecord.Role.INVESTOR:
        InvestorProfile.objects.get_or_create(user=user, defaults=common)
    elif role == RegistrationRecord.Role.SERVICE_PROVIDER:
        ServiceProviderProfile.objects.get_or_create(user=user, defaults=common)
    elif role == RegistrationRecord.Role.PARTNER:
        PartnerProfile.objects.get_or_create(user=user, defaults=common)
    elif role == RegistrationRecord.Role.BUYER:
        BuyerProfile.objects.get_or_create(user=user, defaults=common)


@transaction.atomic
def admin_backend_register(
    *, admin_user, email: str, full_name: str, role: str, country: str = "",
    organisation: str = "", phone: str = "", initial_password: str | None = None,
) -> User:
    """FRSME-OBR006 — assisted/bulk onboarding bypassing email verification.

    Audit fix: an unconsumed self-registration leaves an inactive ``User`` shell
    behind via ``record_recipient_proxy``. Treat that shell as adoptable rather
    than a blocking collision so the admin flow doesn't 500 after a stale
    self-register attempt. We still block when an *active* user already owns
    the email (genuine duplicate).
    """
    email_norm = email.strip().lower()
    existing_active = (
        User.objects.filter(email__iexact=email_norm, is_active=True).exists()
    )
    if existing_active:
        raise DuplicateEmailError(email_norm)

    user = (
        User.objects.filter(email__iexact=email_norm, is_active=False).first()
    )
    if user is None:
        user = User(email=email_norm)
    user.first_name = full_name.split(" ")[0]
    user.last_name = " ".join(full_name.split(" ")[1:])
    user.phone = phone
    user.is_active = True
    user.email_verified = True
    user.role = ROLE_TO_USERROLE[role]
    if initial_password:
        user.set_password(initial_password)
    else:
        user.set_unusable_password()
    user.save()

    # Reconcile any stale registration record that pointed at this shell user
    # so resend_verification doesn't pick it up after the admin path completes.
    RegistrationRecord.objects.filter(
        email__iexact=email_norm, consumed_at__isnull=True,
    ).update(consumed_at=timezone.now())

    _ensure_role_profile(user, role)
    _audit(admin_user, AuditLog.Action.CREATE, user, metadata={"event": "smehub_admin_backend_register"})
    return user


# ---------------------------------------------------------------------------
# RIMS beneficiary lookup  (FRSME-OBR011 / OBR012 / A2)
# ---------------------------------------------------------------------------

def lookup_rims_match(email: str) -> dict[str, Any]:
    """Return any RIMS beneficiary records matched by the registered email.

    The dict has keys `applications` (list[Application]) and `scholars`
    (list[Scholar]).  Either may be empty.  When at least one entry exists,
    the entrepreneur profile may be pre-populated (FRSME-OBR012).  Where
    multiple matches exist, FRSME-OBR032 alternative flow A7 mandates that
    the user picks the correct one (handled in the view layer).
    """
    from apps.rims.grants.models import Application
    from apps.rims.scholarships.models import Scholar

    email_norm = email.strip().lower()
    applications = list(
        Application.objects.filter(applicant__email__iexact=email_norm)
        .select_related("call", "applicant")
    )
    scholars = list(
        Scholar.objects.filter(user__email__iexact=email_norm)
        .select_related("scholarship", "user", "award")
    )
    return {"applications": applications, "scholars": scholars}


# ---------------------------------------------------------------------------
# RIMS prefill (FRSME-OBR012)
# ---------------------------------------------------------------------------

def _rims_prefill_for(
    *,
    application_id: int | None = None,
    scholar_id: int | None = None,
) -> dict[str, str]:
    """Resolve a confirmed RIMS match into prefill values for the
    entrepreneur profile. Returns ``{"country": ..., "organisation": ...,
    "phone": ...}`` with only the fields RIMS actually stores; missing
    fields are simply absent from the dict.
    """
    if not application_id and not scholar_id:
        return {}
    out: dict[str, str] = {}
    if application_id:
        try:
            from apps.rims.grants.models import Application as RimsApplication

            app = (
                RimsApplication.objects.select_related("applicant", "institution")
                .filter(pk=application_id)
                .first()
            )
        except Exception:
            app = None
        if app is not None:
            inst = getattr(app, "institution", None)
            if inst is not None and getattr(inst, "name", ""):
                out["organisation"] = inst.name
            applicant = getattr(app, "applicant", None)
            if applicant is not None:
                phone = getattr(applicant, "phone", "") or ""
                if phone:
                    out["phone"] = phone
                country = (
                    getattr(applicant, "country", "")
                    or getattr(getattr(applicant, "profile", None), "country", "")
                    or ""
                )
                if country:
                    out["country"] = country
    if scholar_id and not out:
        try:
            from apps.rims.scholarships.models import Scholar
        except Exception:
            return out
        sch = (
            Scholar.objects.select_related("user", "scholarship")
            .filter(pk=scholar_id)
            .first()
        )
        if sch is not None and getattr(sch, "user", None) is not None:
            phone = getattr(sch.user, "phone", "") or ""
            if phone:
                out["phone"] = phone
            country = getattr(sch.user, "country", "") or ""
            if country:
                out["country"] = country
    return out


# ---------------------------------------------------------------------------
# RIMS Award resolver (FRSME-OBR032)
# ---------------------------------------------------------------------------

class AmbiguousAwardMatchError(Exception):
    """Raised when more than one RIMS Award matches a programme-of-origin
    string for an entrepreneur. PRD A7 — view layer must surface options.

    The candidate ``Award`` rows are exposed via ``self.candidates``.
    """

    def __init__(self, candidates):
        self.candidates = list(candidates)
        super().__init__(f"{len(self.candidates)} RIMS awards match")


def resolve_rims_award_for_origin(
    *, email: str, programme_of_origin: str,
):
    """Resolve ``programme_of_origin`` (free-text on the AIH affiliation form)
    to a concrete ``rims.grants.Award`` row for an entrepreneur identified by
    ``email``.

    Returns:
        ``None`` — no candidate award found (caller proceeds without linkage).
        ``Award`` — single unambiguous match (caller stores ``rims_award``).

    Raises:
        ``AmbiguousAwardMatchError`` — more than one award matches; view
        layer must present options to the entrepreneur or escalate to admin
        per PRD alt-flow A7.

    Match strategy:
      1. Find every ``rims.grants.Application`` whose ``applicant.email``
         matches the entrepreneur (case-insensitive).
      2. Of those that have an associated ``Award``, narrow by the call
         title containing ``programme_of_origin`` (case-insensitive).
      3. If still empty, fall back to ANY award for that email (so a vague
         programme name doesn't lose data when the user is the unambiguous
         beneficiary of a single award).
    """
    from apps.rims.grants.models import Award

    email_norm = (email or "").strip().lower()
    needle = (programme_of_origin or "").strip()
    if not email_norm:
        return None

    base_qs = (
        Award.objects.filter(application__applicant__email__iexact=email_norm)
        .select_related("application", "application__call")
    )
    if not base_qs.exists():
        return None

    if needle:
        narrowed = list(base_qs.filter(application__call__title__icontains=needle))
        if len(narrowed) == 1:
            return narrowed[0]
        if len(narrowed) > 1:
            raise AmbiguousAwardMatchError(narrowed)

    awards = list(base_qs)
    if len(awards) == 1:
        return awards[0]
    raise AmbiguousAwardMatchError(awards)


# ---------------------------------------------------------------------------
# Entrepreneur profile completion + verification  (FRSME-OBR013 → OBR020)
# ---------------------------------------------------------------------------

@transaction.atomic
def submit_entrepreneur_profile(
    *,
    user,
    country: str = "",
    organisation: str = "",
    phone: str = "",
    bio: str = "",
    rims_match_application_id: int | None = None,
    rims_match_scholar_id: int | None = None,
) -> EntrepreneurProfile:
    profile, _ = EntrepreneurProfile.objects.get_or_create(user=user)
    # Resubmission path: rejected profiles must transition through `resubmit()`
    # before they can be re-evaluated.
    if profile.verification_status == EntrepreneurProfile.Status.REJECTED:
        profile.resubmit()

    # FRSME-OBR012 — when the user confirms a RIMS match, copy fields the
    # user did not explicitly override. Audit found that the wizard stored
    # the FK but never propagated the matched values into the profile.
    rims_prefill = _rims_prefill_for(
        application_id=rims_match_application_id,
        scholar_id=rims_match_scholar_id,
    )

    profile.country = country or rims_prefill.get("country") or profile.country
    profile.organisation = (
        organisation or rims_prefill.get("organisation") or profile.organisation
    )
    profile.phone = phone or rims_prefill.get("phone") or profile.phone
    profile.bio = bio or profile.bio
    if rims_match_application_id:
        profile.rims_match_application_id = rims_match_application_id
        profile.rims_match_confirmed = True
    if rims_match_scholar_id:
        profile.rims_match_scholar_id = rims_match_scholar_id
        profile.rims_match_confirmed = True
    profile.submitted_at = timezone.now()
    profile.save()
    smehub_signals.smehub_entrepreneur_submitted.send(
        sender=EntrepreneurProfile, profile=profile,
    )
    _notify_admins_pending_entrepreneur(profile)
    _audit(user, AuditLog.Action.UPDATE, profile, metadata={"event": "smehub_entrepreneur_submitted"})
    return profile


def _notify_admins_pending_entrepreneur(profile: EntrepreneurProfile) -> None:
    admins = User.objects.filter(
        is_active=True,
        role__in=[UserRole.ADMIN, UserRole.SYSTEM_ADMIN, UserRole.SME_ADMIN],
    )
    try:
        path = reverse("smehub_onboarding:entrepreneur_queue")
    except NoReverseMatch:
        path = "/smehub/admin/queue/entrepreneurs/"
    bulk_notify_smehub(
        admins,
        message=(
            f"New entrepreneur profile awaiting verification: "
            f"{profile.user.get_full_name()} ({profile.user.email})."
        ),
        verb=Notification.Verb.SMEHUB_ENTREPRENEUR_PENDING_REVIEW,
        action_url=path,
    )


@transaction.atomic
def verify_entrepreneur(profile: EntrepreneurProfile, *, by_user) -> EntrepreneurProfile:
    profile.verify(by_user=by_user)
    profile.save()
    notify_smehub(
        profile.user,
        message="Your entrepreneur profile has been verified. You may now register a business.",
        verb=Notification.Verb.SMEHUB_ENTREPRENEUR_VERIFIED,
        action_url="/smehub/business/new/",
        target=profile,
    )
    smehub_signals.smehub_entrepreneur_verified.send(
        sender=EntrepreneurProfile, profile=profile, by_user=by_user,
    )
    _audit(by_user, AuditLog.Action.UPDATE, profile, metadata={"event": "smehub_entrepreneur_verified"})
    return profile


@transaction.atomic
def reject_entrepreneur(profile: EntrepreneurProfile, *, by_user, reason: str) -> EntrepreneurProfile:
    if not reason.strip():
        raise ValueError("rejection reason is mandatory (FRSME-OBR018).")
    profile.reject(reason=reason)
    profile.save()
    notify_smehub(
        profile.user,
        message=f"Your profile was not accepted. Reason: {reason}. You may resubmit with corrections.",
        verb=Notification.Verb.SMEHUB_ENTREPRENEUR_REJECTED,
        target=profile,
    )
    smehub_signals.smehub_entrepreneur_rejected.send(
        sender=EntrepreneurProfile, profile=profile, by_user=by_user, reason=reason,
    )
    _audit(by_user, AuditLog.Action.UPDATE, profile, metadata={"event": "smehub_entrepreneur_rejected", "reason": reason})
    return profile


@transaction.atomic
def request_more_info_from_entrepreneur(
    profile: EntrepreneurProfile, *, by_user, message: str,
) -> EntrepreneurProfile:
    if not message.strip():
        raise ValueError("information request must include a message.")
    profile.request_more_info(message=message)
    profile.save()
    notify_smehub(
        profile.user,
        message=f"More information requested: {message}",
        verb=Notification.Verb.SMEHUB_ENTREPRENEUR_MORE_INFO,
        target=profile,
    )
    smehub_signals.smehub_entrepreneur_more_info_requested.send(
        sender=EntrepreneurProfile, profile=profile, by_user=by_user, message=message,
    )
    _audit(by_user, AuditLog.Action.UPDATE, profile, metadata={"event": "smehub_more_info_requested"})
    return profile


@transaction.atomic
def bulk_verify_entrepreneurs(profile_ids: Iterable[int], *, by_user) -> int:
    """FRSME-OBR020 — cohort-style bulk verification."""
    qs = EntrepreneurProfile.objects.filter(
        pk__in=list(profile_ids),
        verification_status__in=[
            EntrepreneurProfile.Status.PENDING_VERIFICATION,
            EntrepreneurProfile.Status.PENDING_MORE_INFO,
        ],
    )
    count = 0
    for profile in qs:
        verify_entrepreneur(profile, by_user=by_user)
        count += 1
    return count


# ---------------------------------------------------------------------------
# Business registration & verification  (FRSME-OBR021 → OBR029)
# ---------------------------------------------------------------------------

class EntrepreneurNotVerifiedError(Exception):
    pass


@transaction.atomic
def register_business(
    *,
    entrepreneur: EntrepreneurProfile,
    name: str,
    sector: str,
    sub_sector: str = "",
    business_stage: str,
    founding_date=None,
    country: str = "",
    location: str = "",
    description: str = "",
    target_market: str = "",
    # baseline fields (FRSME-OBR023)
    fte_count: int = 0,
    pte_count: int = 0,
    revenue_range: str = "none",
    primary_target_market: str = "",
    geographic_reach: str = "",
) -> Business:
    if entrepreneur.verification_status != EntrepreneurProfile.Status.VERIFIED:
        raise EntrepreneurNotVerifiedError(entrepreneur.pk)
    business = Business.objects.create(
        entrepreneur=entrepreneur,
        name=name,
        sector=sector,
        sub_sector=sub_sector,
        business_stage=business_stage,
        founding_date=founding_date,
        country=country or entrepreneur.country,
        location=location,
        description=description,
        target_market=target_market,
    )
    BusinessBaseline.objects.create(
        business=business,
        business_stage_at_entry=business_stage,
        fte_count=fte_count,
        pte_count=pte_count,
        revenue_range=revenue_range,
        primary_target_market=primary_target_market,
        geographic_reach=geographic_reach,
    )
    smehub_signals.smehub_business_registered.send(sender=Business, business=business)
    _notify_admins_pending_business(business)
    _audit(entrepreneur.user, AuditLog.Action.CREATE, business, metadata={"event": "smehub_business_registered"})
    return business


def _notify_admins_pending_business(business: Business) -> None:
    admins = User.objects.filter(
        is_active=True,
        role__in=[UserRole.ADMIN, UserRole.SYSTEM_ADMIN, UserRole.SME_ADMIN],
    )
    try:
        path = reverse("smehub_onboarding:business_queue")
    except NoReverseMatch:
        path = "/smehub/admin/queue/businesses/"
    bulk_notify_smehub(
        admins,
        message=(
            f"New business {business.business_id} ({business.name}, sector "
            f"{business.sector}) awaiting verification."
        ),
        verb=Notification.Verb.SMEHUB_BUSINESS_PENDING_REVIEW,
        action_url=path,
    )


@transaction.atomic
def verify_business(business: Business, *, by_user) -> Business:
    business.verify(by_user=by_user)
    business.save()
    notify_smehub(
        business.entrepreneur.user,
        message=f"Your business {business.name} ({business.business_id}) is now verified. Complete AIH affiliation next.",
        verb=Notification.Verb.SMEHUB_BUSINESS_VERIFIED,
        action_url=f"/smehub/business/{business.pk}/affiliation/",
        target=business,
    )
    smehub_signals.smehub_business_verified.send(sender=Business, business=business, by_user=by_user)
    _audit(by_user, AuditLog.Action.UPDATE, business, metadata={"event": "smehub_business_verified"})
    return business


@transaction.atomic
def reject_business(business: Business, *, by_user, reason: str) -> Business:
    if not reason.strip():
        raise ValueError("rejection reason is mandatory (FRSME-OBR028).")
    business.reject(reason=reason)
    business.save()
    notify_smehub(
        business.entrepreneur.user,
        message=f"Business {business.name} was not accepted. Reason: {reason}.",
        verb=Notification.Verb.SMEHUB_BUSINESS_REJECTED,
        target=business,
    )
    smehub_signals.smehub_business_rejected.send(
        sender=Business, business=business, by_user=by_user, reason=reason,
    )
    _audit(by_user, AuditLog.Action.UPDATE, business, metadata={"event": "smehub_business_rejected", "reason": reason})
    return business


@transaction.atomic
def revoke_business(business: Business, *, by_user, reason: str) -> Business:
    if not reason.strip():
        raise ValueError("revocation reason is mandatory (FRSME-OBR029).")
    business.revoke(reason=reason)
    business.save()
    notify_smehub(
        business.entrepreneur.user,
        message=f"Business {business.name} ({business.business_id}) has been revoked. Reason: {reason}.",
        verb=Notification.Verb.SMEHUB_BUSINESS_REVOKED,
        target=business,
    )
    smehub_signals.smehub_business_revoked.send(
        sender=Business, business=business, by_user=by_user, reason=reason,
    )
    _audit(by_user, AuditLog.Action.UPDATE, business, metadata={"event": "smehub_business_revoked", "reason": reason})
    return business


# ---------------------------------------------------------------------------
# AIH affiliation + M&E baseline lock  (FRSME-OBR030 → OBR036)
# ---------------------------------------------------------------------------

@transaction.atomic
def record_aih_affiliation(
    *,
    business: Business,
    institution_id: int,
    nature: str,
    programme_of_origin: str = "",
    is_primary: bool = True,
    rims_award_id: int | None = None,
    rims_scholar_id: int | None = None,
    by_admin=None,
    resolve_award: bool = True,
) -> AIHAffiliation:
    """Create an affiliation row.  When `is_primary=True`, demote any existing
    primary on the same business (FRSME-OBR033).  When the programme of
    origin maps to a RIMS award, the cross-reference is stored (FRSME-OBR032).

    ``resolve_award=False`` skips the auto award-resolution (callers that have
    already resolved — e.g. the admin correction path — pass their own
    ``rims_award_id`` and avoid re-raising ``AmbiguousAwardMatchError``).
    """
    # FRSME-OBR032 — when the caller has not already pinned an award (e.g. via
    # the A7 ambiguity picker), resolve the free-text programme of origin to a
    # concrete RIMS Award for this entrepreneur. A single unambiguous match is
    # stored transparently; multiple matches raise ``AmbiguousAwardMatchError``,
    # which the view surfaces as the "pick the correct award" panel.
    if resolve_award and rims_award_id is None and programme_of_origin:
        matched_award = resolve_rims_award_for_origin(
            email=business.entrepreneur.user.email,
            programme_of_origin=programme_of_origin,
        )
        if matched_award is not None:
            rims_award_id = matched_award.pk

    if is_primary:
        AIHAffiliation.objects.filter(business=business, is_primary=True).update(is_primary=False)
    affiliation = AIHAffiliation.objects.create(
        business=business,
        institution_id=institution_id,
        nature=nature,
        programme_of_origin=programme_of_origin,
        is_primary=is_primary,
        rims_award_id=rims_award_id,
        rims_scholarship_record_id=rims_scholar_id,
        set_by_admin=by_admin is not None,
    )
    smehub_signals.smehub_aih_affiliation_recorded.send(
        sender=AIHAffiliation, business=business, affiliation=affiliation,
    )
    _audit(
        by_admin or business.entrepreneur.user,
        AuditLog.Action.CREATE,
        affiliation,
        metadata={"event": "smehub_aih_affiliation_recorded"},
    )
    # Lock baseline once at least one affiliation exists.
    _lock_baseline_if_ready(business)
    return affiliation


def _safe_resolve_award_id(business: Business, programme_of_origin: str) -> int | None:
    """Resolve a programme-of-origin to a RIMS award id, tolerating ambiguity.

    Unlike the applicant flow, the admin correction path must never 500 on an
    ambiguous match — it simply leaves the award unresolved for later.
    """
    if not programme_of_origin:
        return None
    try:
        award = resolve_rims_award_for_origin(
            email=business.entrepreneur.user.email,
            programme_of_origin=programme_of_origin,
        )
    except AmbiguousAwardMatchError:
        return None
    return award.pk if award is not None else None


@transaction.atomic
def set_aih_affiliation_by_admin(
    *,
    business: Business,
    institution_id: int,
    nature: str,
    programme_of_origin: str = "",
    is_primary: bool = True,
    admin,
) -> AIHAffiliation:
    """FRSME-OBR034 — an administrator sets or corrects a business's AIH
    affiliation from the backend. Idempotent per (business, institution): a
    fresh institution is recorded via ``record_aih_affiliation``; re-submitting
    an existing institution updates it in place (both audit-logged as an admin
    override).
    """
    award_id = _safe_resolve_award_id(business, programme_of_origin)
    existing = AIHAffiliation.objects.filter(
        business=business, institution_id=institution_id,
    ).first()
    if existing is None:
        return record_aih_affiliation(
            business=business,
            institution_id=institution_id,
            nature=nature,
            programme_of_origin=programme_of_origin,
            is_primary=is_primary,
            rims_award_id=award_id,
            by_admin=admin,
            resolve_award=False,
        )
    if is_primary:
        AIHAffiliation.objects.filter(business=business, is_primary=True).exclude(
            pk=existing.pk,
        ).update(is_primary=False)
    existing.nature = nature
    existing.programme_of_origin = programme_of_origin
    existing.is_primary = is_primary
    existing.set_by_admin = True
    if award_id is not None:
        existing.rims_award_id = award_id
    existing.save()
    _audit(
        admin,
        AuditLog.Action.UPDATE,
        existing,
        metadata={"event": "smehub_aih_affiliation_admin_corrected"},
    )
    return existing


@transaction.atomic
def _lock_baseline_if_ready(business: Business) -> None:
    """Emit `smehub_baseline_initialised` exactly once per business
    (FRSME-OBR035, FRSME-OBR036).  Idempotent.
    """
    baseline = getattr(business, "baseline", None)
    if baseline is None:
        return
    if baseline.locked_at is not None:
        return
    if not business.affiliations.exists():
        return
    baseline.locked_at = timezone.now()
    baseline.save(update_fields=["locked_at", "updated_at"])
    smehub_signals.smehub_baseline_initialised.send(
        sender=BusinessBaseline,
        business=business,
        baseline=baseline,
        entrepreneur=business.entrepreneur,
    )


# ---------------------------------------------------------------------------
# Generic role profile verification (Mentor / Investor / ServiceProvider /
# Partner / Buyer)  — FRSME-OBR-A9 (non-entrepreneur roles complete here)
# ---------------------------------------------------------------------------

ROLE_PROFILE_MODELS = {
    "mentor": MentorProfile,
    "investor": InvestorProfile,
    "service_provider": ServiceProviderProfile,
    "partner": PartnerProfile,
    "buyer": BuyerProfile,
}


@transaction.atomic
def verify_role_profile(profile, *, by_user):
    profile.verify(by_user=by_user)
    profile.save()
    notify_smehub(
        profile.user,
        message="Your profile has been verified. You can now use the SME-Hub.",
        verb=Notification.Verb.SMEHUB_ROLE_PROFILE_VERIFIED,
        target=profile,
    )
    smehub_signals.smehub_role_profile_verified.send(
        sender=profile.__class__, profile=profile, by_user=by_user,
    )
    _audit(by_user, AuditLog.Action.UPDATE, profile, metadata={"event": "smehub_role_profile_verified"})
    return profile


@transaction.atomic
def reject_role_profile(profile, *, by_user, reason: str):
    if not reason.strip():
        raise ValueError("rejection reason is mandatory.")
    profile.reject(reason=reason)
    profile.save()
    notify_smehub(
        profile.user,
        message=f"Your profile was not accepted. Reason: {reason}.",
        verb=Notification.Verb.SMEHUB_ENTREPRENEUR_REJECTED,
        target=profile,
    )
    smehub_signals.smehub_role_profile_rejected.send(
        sender=profile.__class__, profile=profile, by_user=by_user, reason=reason,
    )
    _audit(by_user, AuditLog.Action.UPDATE, profile, metadata={"event": "smehub_role_profile_rejected", "reason": reason})
    return profile


# ---------------------------------------------------------------------------
# Innovation lifecycle  (Phase 6.1)
# ---------------------------------------------------------------------------


class InnovationStateError(Exception):
    """Raised on illegal innovation lifecycle / vetting transitions."""


_INNOVATION_DRAFT_FIELDS = {
    "title",
    "tagline",
    "description",
    "problem_statement",
    "solution_summary",
    "market_focus",
    "target_segments",
    "sector",
    "sub_sector",
    "country",
    "stage",
    "team_size",
    "team_summary",
    "cover_image",
    "pitch_deck",
    "business_model_canvas",
}


def _notify_sme_admins(message: str, *, verb: str, target=None) -> None:
    admins = User.objects.filter(
        is_active=True,
        role__in=[UserRole.ADMIN, UserRole.SYSTEM_ADMIN, UserRole.SME_ADMIN],
    )
    bulk_notify_smehub(list(admins), message, verb=verb)


@transaction.atomic
def create_innovation_draft(*, business: Business, by_user, **fields) -> Innovation:
    """Entrepreneur creates a draft Innovation against a verified Business."""
    if business.verification_status != Business.Status.VERIFIED:
        raise InnovationStateError("Business must be VERIFIED before adding innovations.")
    payload = {k: v for k, v in fields.items() if k in _INNOVATION_DRAFT_FIELDS}
    if not payload.get("title"):
        raise ValueError("title is required.")
    innovation = Innovation.objects.create(business=business, **payload)
    _audit(by_user, AuditLog.Action.CREATE, innovation, metadata={"event": "innovation_drafted"})
    smehub_signals.smehub_innovation_drafted.send(
        sender=Innovation, innovation=innovation, by_user=by_user,
    )
    return innovation


@transaction.atomic
def update_innovation_draft(innovation: Innovation, *, by_user, **fields) -> Innovation:
    if innovation.lifecycle_status not in {
        Innovation.Status.DRAFT,
        Innovation.Status.REVISIONS_REQUESTED,
    }:
        raise InnovationStateError(
            "Innovation can only be edited while in DRAFT or REVISIONS_REQUESTED."
        )
    changed: list[str] = []
    for key, value in fields.items():
        if key in _INNOVATION_DRAFT_FIELDS and getattr(innovation, key) != value:
            setattr(innovation, key, value)
            changed.append(key)
    if changed:
        innovation.save(update_fields=changed + ["updated_at"])
        _audit(
            by_user,
            AuditLog.Action.UPDATE,
            innovation,
            metadata={"event": "innovation_updated", "fields": changed},
        )
    return innovation


@transaction.atomic
def submit_innovation(innovation: Innovation, *, by_user) -> Innovation:
    """Transition DRAFT/REVISIONS_REQUESTED → SUBMITTED."""
    innovation.submit(by_user=by_user)
    innovation.save()
    _audit(by_user, AuditLog.Action.UPDATE, innovation, metadata={"event": "innovation_submitted"})
    smehub_signals.smehub_innovation_submitted.send(
        sender=Innovation, innovation=innovation, by_user=by_user,
    )
    _notify_sme_admins(
        f"Innovation '{innovation.title}' is awaiting vetting.",
        verb=Notification.Verb.SMEHUB_INNOVATION_SUBMITTED,
        target=innovation,
    )
    return innovation


@transaction.atomic
def assign_evaluator(
    innovation: Innovation,
    *,
    evaluator,
    by_user,
    due_at=None,
) -> EvaluatorAssignment:
    """Admin-only. First assignment auto-advances SUBMITTED → UNDER_VETTING."""
    if innovation.lifecycle_status not in {
        Innovation.Status.SUBMITTED,
        Innovation.Status.UNDER_VETTING,
    }:
        raise InnovationStateError(
            "Evaluators can only be assigned to SUBMITTED or UNDER_VETTING innovations."
        )
    assignment, created = EvaluatorAssignment.objects.get_or_create(
        innovation=innovation,
        evaluator=evaluator,
        defaults={
            "assigned_by": by_user,
            "due_at": due_at,
            "status": EvaluatorAssignment.Status.ASSIGNED,
        },
    )
    if not created and assignment.status == EvaluatorAssignment.Status.RECUSED:
        assignment.status = EvaluatorAssignment.Status.ASSIGNED
        assignment.assigned_by = by_user
        assignment.assigned_at = timezone.now()
        assignment.due_at = due_at
        assignment.recusal_reason = ""
        assignment.save()
    if innovation.lifecycle_status == Innovation.Status.SUBMITTED:
        innovation.start_vetting(by_user=by_user)
        innovation.save()
        VettingRecord.objects.get_or_create(
            innovation=innovation,
            round_no=_next_round_no(innovation),
            defaults={"started_at": timezone.now()},
        )
        smehub_signals.smehub_innovation_vetting_started.send(
            sender=Innovation, innovation=innovation, by_user=by_user,
        )
    smehub_signals.smehub_innovation_evaluator_assigned.send(
        sender=Innovation,
        innovation=innovation,
        assignment=assignment,
        by_user=by_user,
    )
    _audit(
        by_user,
        AuditLog.Action.CREATE if created else AuditLog.Action.UPDATE,
        assignment,
        metadata={"event": "evaluator_assigned"},
    )
    notify_smehub(
        evaluator,
        f"You've been assigned to vet innovation '{innovation.title}'.",
        verb=Notification.Verb.SMEHUB_INNOVATION_EVALUATOR_ASSIGNED,
        target=innovation,
    )
    return assignment


def _next_round_no(innovation: Innovation) -> int:
    last = innovation.vetting_records.order_by("-round_no").first()
    return (last.round_no + 1) if last and last.closed_at else (last.round_no if last else 1)


def _current_round(innovation: Innovation) -> VettingRecord | None:
    return innovation.vetting_records.filter(closed_at__isnull=True).order_by("-round_no").first()


@transaction.atomic
def score_vetting_dimension(
    *,
    innovation: Innovation,
    evaluator,
    dimension: str,
    score: int,
    comments: str = "",
) -> VettingScore:
    """Upsert one VettingScore for the evaluator's open vetting round."""
    if innovation.lifecycle_status != Innovation.Status.UNDER_VETTING:
        raise InnovationStateError("Innovation must be UNDER_VETTING to score.")
    if not (1 <= int(score) <= 5):
        raise ValueError("score must be in 1..5.")
    if dimension not in dict(VettingScore.Dimension.choices):
        raise ValueError(f"unknown dimension: {dimension}")
    assignment = innovation.evaluator_assignments.filter(evaluator=evaluator).first()
    if assignment is None:
        raise InnovationStateError("Evaluator is not assigned to this innovation.")
    if assignment.status == EvaluatorAssignment.Status.RECUSED:
        raise InnovationStateError("Recused evaluators cannot score.")
    record = _current_round(innovation)
    if record is None:
        record = VettingRecord.objects.create(
            innovation=innovation,
            round_no=_next_round_no(innovation),
            started_at=timezone.now(),
        )
    obj, created = VettingScore.objects.update_or_create(
        vetting_record=record,
        evaluator=evaluator,
        dimension=dimension,
        defaults={
            "score": int(score),
            "comments": comments,
            "submitted_at": timezone.now(),
        },
    )
    if assignment.status == EvaluatorAssignment.Status.ASSIGNED:
        assignment.status = EvaluatorAssignment.Status.IN_PROGRESS
        assignment.save(update_fields=["status"])
    _audit(
        evaluator,
        AuditLog.Action.CREATE if created else AuditLog.Action.UPDATE,
        obj,
        metadata={"event": "vetting_scored", "dimension": dimension, "score": int(score)},
    )
    smehub_signals.smehub_innovation_scored.send(
        sender=Innovation,
        innovation=innovation,
        vetting_record=record,
        evaluator=evaluator,
    )
    return obj


@transaction.atomic
def submit_vetting_recommendation(
    *,
    innovation: Innovation,
    evaluator,
    recommendation: str,
    summary: str = "",
) -> EvaluatorAssignment:
    """Mark the evaluator's assignment COMPLETED. Closes the round when all done.

    When every active assignment is COMPLETED the round's recommendation is
    set by majority vote (admin tiebreak via vet_application). The round's
    summary captures the last-submitted text.
    """
    if recommendation not in dict(VettingRecord.Recommendation.choices):
        raise ValueError(f"unknown recommendation: {recommendation}")
    assignment = innovation.evaluator_assignments.filter(evaluator=evaluator).first()
    if assignment is None:
        raise InnovationStateError("Evaluator is not assigned to this innovation.")
    if assignment.status == EvaluatorAssignment.Status.RECUSED:
        raise InnovationStateError("Recused evaluators cannot submit a recommendation.")
    record = _current_round(innovation)
    if record is None:
        raise InnovationStateError("No open vetting round.")
    assignment.status = EvaluatorAssignment.Status.COMPLETED
    assignment.completed_at = timezone.now()
    assignment.save(update_fields=["status", "completed_at"])
    record.summary = summary or record.summary
    record.save(update_fields=["summary"])
    # Tally if every active (non-recused) assignment is now COMPLETED.
    active_qs = innovation.evaluator_assignments.exclude(
        status=EvaluatorAssignment.Status.RECUSED
    )
    if active_qs.exists() and all(
        a.status == EvaluatorAssignment.Status.COMPLETED for a in active_qs
    ):
        # Capture the latest recommendation as a default; admin can override.
        record.recommendation = recommendation
        record.save(update_fields=["recommendation"])
    _audit(
        evaluator,
        AuditLog.Action.UPDATE,
        assignment,
        metadata={"event": "vetting_recommendation_submitted", "recommendation": recommendation},
    )
    return assignment


@transaction.atomic
def vet_application(
    innovation: Innovation,
    *,
    by_user,
    decision: str,
    feedback: str = "",
    reason: str = "",
) -> Innovation:
    """Admin finalizes vetting: APPROVE / REJECT / REVISE."""
    record = _current_round(innovation)
    if decision == "approve":
        innovation.approve(by_user=by_user, feedback=feedback)
        innovation.save()
        if record is not None:
            record.recommendation = VettingRecord.Recommendation.APPROVE
            record.closed_at = timezone.now()
            record.closed_by = by_user
            record.save(update_fields=["recommendation", "closed_at", "closed_by"])
        smehub_signals.smehub_innovation_approved.send(
            sender=Innovation, innovation=innovation, by_user=by_user,
        )
        notify_smehub(
            innovation.business.entrepreneur.user,
            f"Innovation '{innovation.title}' has been approved.",
            verb=Notification.Verb.SMEHUB_INNOVATION_DECISION,
            target=innovation,
        )
        _audit(by_user, AuditLog.Action.UPDATE, innovation, metadata={"event": "innovation_approved"})
    elif decision == "reject":
        if not reason.strip():
            raise ValueError("rejection reason is required.")
        innovation.reject(by_user=by_user, reason=reason)
        innovation.save()
        if record is not None:
            record.recommendation = VettingRecord.Recommendation.REJECT
            record.closed_at = timezone.now()
            record.closed_by = by_user
            record.save(update_fields=["recommendation", "closed_at", "closed_by"])
        smehub_signals.smehub_innovation_rejected.send(
            sender=Innovation, innovation=innovation, by_user=by_user, reason=reason,
        )
        notify_smehub(
            innovation.business.entrepreneur.user,
            f"Innovation '{innovation.title}' was rejected. Reason: {reason}.",
            verb=Notification.Verb.SMEHUB_INNOVATION_DECISION,
            target=innovation,
        )
        _audit(by_user, AuditLog.Action.UPDATE, innovation, metadata={"event": "innovation_rejected"})
    elif decision == "revise":
        if not feedback.strip():
            raise ValueError("revision feedback is required.")
        innovation.request_revisions(by_user=by_user, feedback=feedback)
        innovation.save()
        if record is not None:
            record.recommendation = VettingRecord.Recommendation.REVISE
            record.closed_at = timezone.now()
            record.closed_by = by_user
            record.save(update_fields=["recommendation", "closed_at", "closed_by"])
        smehub_signals.smehub_innovation_revisions_requested.send(
            sender=Innovation, innovation=innovation, by_user=by_user, feedback=feedback,
        )
        notify_smehub(
            innovation.business.entrepreneur.user,
            f"Revisions requested for '{innovation.title}': {feedback}",
            verb=Notification.Verb.SMEHUB_INNOVATION_REVISIONS,
            target=innovation,
        )
        _audit(by_user, AuditLog.Action.UPDATE, innovation, metadata={"event": "innovation_revisions"})
    else:
        raise ValueError(f"unknown decision: {decision}")
    return innovation


@transaction.atomic
def onboard(innovation: Innovation, *, by_user) -> Innovation:
    """Publish an APPROVED innovation to the public catalogue. Idempotent."""
    was_public = innovation.is_public
    innovation.onboard(by_user=by_user)
    innovation.save()
    if not was_public and innovation.is_public:
        smehub_signals.smehub_innovation_published.send(
            sender=Innovation, innovation=innovation, by_user=by_user,
        )
        notify_smehub(
            innovation.business.entrepreneur.user,
            f"Your innovation '{innovation.title}' is now live in the public catalogue.",
            verb=Notification.Verb.SMEHUB_INNOVATION_PUBLISHED,
            target=innovation,
        )
        _audit(by_user, AuditLog.Action.UPDATE, innovation, metadata={"event": "innovation_published"})
    return innovation


@transaction.atomic
def withdraw_innovation(innovation: Innovation, *, by_user, reason: str = "") -> Innovation:
    innovation.withdraw(by_user=by_user, reason=reason)
    innovation.save()
    smehub_signals.smehub_innovation_withdrawn.send(
        sender=Innovation, innovation=innovation, by_user=by_user, reason=reason,
    )
    _audit(
        by_user,
        AuditLog.Action.UPDATE,
        innovation,
        metadata={"event": "innovation_withdrawn", "reason": reason},
    )
    return innovation


# ---------------------------------------------------------------------------
# Mentor meeting requests (direct mentorship access for entrepreneurs)
# ---------------------------------------------------------------------------

class MeetingRequestError(Exception):
    """Raised on invalid meeting-request operations (duplicate, wrong state)."""


def request_mentor_meeting(
    *, requester, mentor: MentorProfile, agenda: str, meeting_time=None,
):
    """Create a MeetingRequest from `requester` to a verified mentor."""
    from django.contrib.contenttypes.models import ContentType

    from apps.smehub.onboarding.models import MeetingRequest

    if mentor.verification_status != MentorProfile.Status.VERIFIED:
        raise MeetingRequestError("This mentor profile is not accepting requests.")
    if mentor.user_id == requester.pk:
        raise MeetingRequestError("You cannot request a meeting with yourself.")
    mentor_ct = ContentType.objects.get_for_model(MentorProfile)
    if MeetingRequest.objects.filter(
        requester=requester,
        recipient_content_type=mentor_ct,
        recipient_object_id=mentor.pk,
        status=MeetingRequest.Status.REQUEST,
    ).exists():
        raise MeetingRequestError(
            "You already have a pending request with this mentor — "
            "wait for their reply or cancel it first."
        )
    mr = MeetingRequest.objects.create(
        requester=requester,
        recipient_type=MeetingRequest.RecipientType.MENTOR,
        recipient_content_type=mentor_ct,
        recipient_object_id=mentor.pk,
        meeting_time=meeting_time,
        agenda=agenda,
    )
    notify_smehub(
        mentor.user,
        f"{requester.get_full_name() or requester.email} requested a mentorship meeting.",
        verb=Notification.Verb.SMEHUB_MEETING_REQUESTED,
        action_url=reverse("smehub_onboarding:mentor_meeting_inbox"),
        target=mr,
    )
    _audit(requester, AuditLog.Action.CREATE, mr, metadata={"event": "meeting_requested"})
    return mr


def respond_meeting_request(
    mr, *, by_user, accept: bool, meeting_link: str = "", reason: str = "",
):
    """Mentor accepts or declines a pending request."""
    from apps.smehub.onboarding.models import MeetingRequest

    if mr.status != MeetingRequest.Status.REQUEST:
        raise MeetingRequestError("This request has already been resolved.")
    mr.status = MeetingRequest.Status.ACCEPTED if accept else MeetingRequest.Status.REJECTED
    if accept and meeting_link:
        mr.meeting_link = meeting_link
    if reason:
        mr.reason = reason
    mr.save(update_fields=["status", "meeting_link", "reason", "updated_at"])
    if accept:
        message = "Your mentorship meeting request was accepted."
        if meeting_link:
            message += " A call link has been shared."
        verb = Notification.Verb.SMEHUB_MEETING_ACCEPTED
    else:
        message = "Your mentorship meeting request was declined."
        if reason:
            message += f" Note from the mentor: {reason}"
        verb = Notification.Verb.SMEHUB_MEETING_DECLINED
    notify_smehub(
        mr.requester,
        message,
        verb=verb,
        action_url=reverse("smehub_onboarding:my_meeting_requests"),
        target=mr,
    )
    _audit(by_user, AuditLog.Action.UPDATE, mr,
           metadata={"event": "meeting_responded", "accepted": accept})
    return mr


def cancel_meeting_request(mr, *, by_user):
    """Requester withdraws their own pending request."""
    from apps.smehub.onboarding.models import MeetingRequest

    if mr.requester_id != by_user.pk:
        raise MeetingRequestError("Only the requester can cancel a request.")
    if mr.status != MeetingRequest.Status.REQUEST:
        raise MeetingRequestError("Only pending requests can be cancelled.")
    mr.status = MeetingRequest.Status.CANCELLED
    mr.save(update_fields=["status", "updated_at"])
    _audit(by_user, AuditLog.Action.UPDATE, mr, metadata={"event": "meeting_cancelled"})
    return mr
