"""SME-Hub marketplace services — Subprocess SP3b.

Implements PRD FRSME-MPL014 → MPL016 plus buyer registry verification.
Side-effects (notifications, audit log, signal emission) live here.
"""
from __future__ import annotations

from decimal import Decimal

from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db import transaction
from django.urls import NoReverseMatch, reverse
from django.utils import timezone

from apps.core.money.currencies import DEFAULT_CURRENCY
from apps.core.notifications.models import Notification
from apps.smehub._shared.audit import AuditLog, _audit
from apps.smehub._shared.notifications import bulk_notify_smehub, notify_smehub
from apps.smehub.marketplace import signals as mkt_signals
from apps.smehub.marketplace.models import (
    BuyerRegistryEntry,
    DemandResponse,
    MarketDemandListing,
    MarketValidation,
    ProductListing,
    ProductMedia,
    SalesRecord,
)
from apps.smehub.onboarding.models import Business, EntrepreneurProfile

User = get_user_model()
Verb = Notification.Verb


# ---------------------------------------------------------------------------
# Exceptions
# ---------------------------------------------------------------------------

class MarketplaceStateError(Exception):
    """Raised on illegal marketplace transitions / preconditions."""


def _safe_reverse(name: str, **kwargs) -> str:
    try:
        return reverse(name, kwargs=kwargs) if kwargs else reverse(name)
    except NoReverseMatch:
        return ""


# ---------------------------------------------------------------------------
# Buyer registry
# ---------------------------------------------------------------------------

@transaction.atomic
def register_buyer(*, user, **fields) -> BuyerRegistryEntry:
    entry, created = BuyerRegistryEntry.objects.update_or_create(
        user=user,
        defaults=fields,
    )
    _audit(user, AuditLog.Action.CREATE if created else AuditLog.Action.UPDATE, entry)
    return entry


@transaction.atomic
def register_buyer_self(*, user, **fields) -> BuyerRegistryEntry:
    """Public-facing self-registration. Creates the entry in PENDING and
    promotes the user to the BUYER role if they don't already have a stronger
    SME-Hub role assigned."""
    from apps.core.permissions.roles import UserRole

    entry = register_buyer(user=user, **fields)
    if not user.role or user.role in {"", UserRole.LEARNER, UserRole.APPLICANT}:
        user.role = UserRole.BUYER
        user.save(update_fields=["role"])
    return entry


@transaction.atomic
def verify_buyer(buyer: BuyerRegistryEntry, *, by_user) -> BuyerRegistryEntry:
    buyer.verify(by_user=by_user)
    buyer.save()
    _audit(by_user, AuditLog.Action.UPDATE, buyer, metadata={"status": "verified"})
    mkt_signals.smehub_buyer_verified.send(sender=BuyerRegistryEntry, buyer=buyer, by_user=by_user)
    notify_smehub(
        buyer.user,
        f"Your buyer registry entry “{buyer.organisation_name}” has been verified.",
        verb=Verb.SMEHUB_BUSINESS_VERIFIED,
        target=buyer,
    )
    return buyer


@transaction.atomic
def reject_buyer(buyer: BuyerRegistryEntry, *, by_user, reason: str) -> BuyerRegistryEntry:
    buyer.reject(reason=reason)
    buyer.save()
    _audit(by_user, AuditLog.Action.UPDATE, buyer, metadata={"status": "rejected", "reason": reason})
    notify_smehub(
        buyer.user,
        f"Your buyer registry entry “{buyer.organisation_name}” was rejected: {reason}",
        verb=Verb.SMEHUB_BUSINESS_REJECTED,
        target=buyer,
    )
    return buyer


@transaction.atomic
def deactivate_buyer(buyer: BuyerRegistryEntry, *, by_user, reason: str = "") -> BuyerRegistryEntry:
    """FRSME-MPL001 — admin deactivates a buyer entry."""
    buyer.deactivate(reason=reason)
    buyer.save()
    _audit(by_user, AuditLog.Action.UPDATE, buyer, metadata={"status": "deactivated", "reason": reason})
    notify_smehub(
        buyer.user,
        f"Your buyer registry entry “{buyer.organisation_name}” has been deactivated."
        + (f" Reason: {reason}" if reason else ""),
        verb=Verb.SMEHUB_BUSINESS_REVOKED,
        target=buyer,
    )
    return buyer


@transaction.atomic
def reactivate_buyer(buyer: BuyerRegistryEntry, *, by_user) -> BuyerRegistryEntry:
    buyer.reactivate(by_user=by_user)
    buyer.save()
    _audit(by_user, AuditLog.Action.UPDATE, buyer, metadata={"status": "verified", "reactivated": True})
    notify_smehub(
        buyer.user,
        f"Your buyer registry entry “{buyer.organisation_name}” has been reactivated.",
        verb=Verb.SMEHUB_BUSINESS_VERIFIED,
        target=buyer,
    )
    return buyer


# ---------------------------------------------------------------------------
# Product listings  (FRSME-MPL014)
# ---------------------------------------------------------------------------

@transaction.atomic
def create_product_listing(
    *,
    entrepreneur: EntrepreneurProfile,
    business: Business,
    **fields,
) -> ProductListing:
    if business.verification_status != Business.Status.VERIFIED:
        raise MarketplaceStateError("Business must be verified before listing products.")
    listing = ProductListing.objects.create(
        entrepreneur=entrepreneur,
        business=business,
        **fields,
    )
    _audit(entrepreneur.user, AuditLog.Action.CREATE, listing)
    return listing


@transaction.atomic
def publish_product(listing: ProductListing, *, by_user) -> ProductListing:
    if listing.status == ProductListing.Status.PUBLISHED:
        return listing
    # FRSME-MPL014 — re-check business verification at publish time; a
    # business that was VERIFIED at create time may have been revoked.
    business = listing.business
    if business.verification_status != Business.Status.VERIFIED:
        raise MarketplaceStateError(
            "Cannot publish: the underlying business is not currently verified.",
        )
    listing.status = ProductListing.Status.PUBLISHED
    listing.published_at = timezone.now()
    listing.unpublished_at = None
    listing.save(update_fields=["status", "published_at", "unpublished_at", "updated_at"])
    _audit(by_user, AuditLog.Action.UPDATE, listing, metadata={"status": "published"})
    mkt_signals.smehub_product_published.send(
        sender=ProductListing, listing=listing, by_user=by_user,
    )
    return listing


@transaction.atomic
def unpublish_product(listing: ProductListing, *, by_user) -> ProductListing:
    if listing.status != ProductListing.Status.PUBLISHED:
        return listing
    listing.status = ProductListing.Status.UNPUBLISHED
    listing.unpublished_at = timezone.now()
    listing.save(update_fields=["status", "unpublished_at", "updated_at"])
    _audit(by_user, AuditLog.Action.UPDATE, listing, metadata={"status": "unpublished"})
    mkt_signals.smehub_product_unpublished.send(
        sender=ProductListing, listing=listing, by_user=by_user,
    )
    return listing


@transaction.atomic
def attach_product_media(
    listing: ProductListing,
    *,
    kind: str = ProductMedia.Kind.IMAGE,
    title: str = "",
    file=None,
    external_url: str = "",
    sort_order: int = 0,
) -> ProductMedia:
    if not file and not external_url:
        raise ValidationError("Provide a file or an external URL.")
    return ProductMedia.objects.create(
        listing=listing,
        kind=kind,
        title=title,
        file=file,
        external_url=external_url,
        sort_order=sort_order,
    )


@transaction.atomic
def express_product_interest(listing: ProductListing, *, from_user, message: str = "") -> None:
    """In-app "express interest" — notifies the seller (in-app + email) so the
    workflow works even when the buyer has no desktop mail client configured.
    """
    seller = listing.entrepreneur.user
    sender_name = from_user.get_full_name() or from_user.email
    body = (
        f"{sender_name} ({from_user.email}) expressed interest in your product "
        f"“{listing.name}”."
    )
    if message.strip():
        body += f' Message: "{message.strip()[:500]}"'
    notify_smehub(
        seller,
        body,
        verb=Verb.SMEHUB_PRODUCT_INTEREST,
        action_url=_safe_reverse("smehub_marketplace:product_detail", pk=listing.pk),
        target=listing,
    )
    _audit(from_user, AuditLog.Action.CREATE, listing, metadata={"event": "product_interest_expressed"})


# ---------------------------------------------------------------------------
# Market demand  (FRSME-MPL015 / MPL016)
# ---------------------------------------------------------------------------

@transaction.atomic
def post_market_demand(
    *,
    buyer: BuyerRegistryEntry,
    title: str,
    description: str,
    kind: str = MarketDemandListing.Kind.PROBLEM,
    sector_targeting=None,
    geographic_targeting=None,
    quantity: str = "",
    deadline=None,
) -> MarketDemandListing:
    if buyer.verification_status != BuyerRegistryEntry.Status.VERIFIED:
        raise MarketplaceStateError("Buyer must be verified before posting demand.")
    listing = MarketDemandListing.objects.create(
        buyer=buyer,
        title=title,
        description=description,
        kind=kind,
        sector_targeting=list(sector_targeting or []),
        geographic_targeting=list(geographic_targeting or []),
        quantity=quantity,
        deadline=deadline,
    )
    _audit(buyer.user, AuditLog.Action.CREATE, listing)
    mkt_signals.smehub_market_demand_posted.send(
        sender=MarketDemandListing, listing=listing,
    )
    _notify_matched_entrepreneurs(listing)
    return listing


def _notify_matched_entrepreneurs(listing: MarketDemandListing) -> int:
    """FRSME-MPL015 — fan-out to entrepreneurs whose verified businesses
    overlap on sector / country.
    """
    qs = Business.objects.filter(verification_status=Business.Status.VERIFIED)
    if listing.sector_targeting:
        qs = qs.filter(sector__in=listing.sector_targeting)
    if listing.geographic_targeting:
        qs = qs.filter(country__in=listing.geographic_targeting)
    user_ids = (
        qs.select_related("entrepreneur__user")
        .values_list("entrepreneur__user_id", flat=True)
        .distinct()
    )
    users = list(User.objects.filter(pk__in=user_ids, is_active=True))
    if not users:
        return 0
    bulk_notify_smehub(
        users,
        f"New market demand: “{listing.title}” from {listing.buyer.organisation_name}.",
        verb=Verb.SMEHUB_MARKET_DEMAND_MATCH,
        action_url=_safe_reverse("smehub_marketplace:demand_detail", pk=listing.pk),
    )
    return len(users)


@transaction.atomic
def close_market_demand(listing: MarketDemandListing, *, by_user) -> MarketDemandListing:
    if listing.status == MarketDemandListing.Status.CLOSED:
        return listing
    listing.status = MarketDemandListing.Status.CLOSED
    listing.closed_at = timezone.now()
    listing.save(update_fields=["status", "closed_at", "updated_at"])
    _audit(by_user, AuditLog.Action.UPDATE, listing, metadata={"status": "closed"})
    mkt_signals.smehub_market_demand_closed.send(
        sender=MarketDemandListing, listing=listing, by_user=by_user,
    )
    return listing


def close_expired_demand_listings(now=None) -> int:
    """Audit fix — auto-close OPEN listings whose deadline has passed.

    Without this sweeper an entrepreneur could still respond to a listing
    after its deadline (the only check was on ``status == OPEN`` and the
    listing was never auto-flipped). Returns the count of listings closed.

    Idempotent — listings already CLOSED are skipped.
    """
    now = now or timezone.now()
    qs = MarketDemandListing.objects.filter(
        status=MarketDemandListing.Status.OPEN,
        deadline__lt=now,
    ).exclude(deadline__isnull=True)
    closed = 0
    for listing in qs:
        listing.status = MarketDemandListing.Status.CLOSED
        listing.closed_at = now
        listing.save(update_fields=["status", "closed_at", "updated_at"])
        _audit(None, AuditLog.Action.UPDATE, listing, metadata={"event": "auto_closed", "deadline": listing.deadline.isoformat()})
        owner = getattr(getattr(listing, "buyer", None), "user", None)
        if owner is not None:
            try:
                notify_smehub(
                    owner,
                    f"Demand listing \"{listing.title}\" closed automatically (deadline reached).",
                    verb=Verb.SMEHUB_DEMAND_RESPONSE,  # buyer-side system event
                    target=listing,
                )
            except Exception:
                pass
        closed += 1
    return closed


@transaction.atomic
def respond_to_demand(
    *,
    demand_listing: MarketDemandListing,
    entrepreneur: EntrepreneurProfile,
    business: Business,
    message: str,
    pricing_offer: str = "",
    attachment=None,
    product_listing: ProductListing | None = None,
) -> DemandResponse:
    if demand_listing.status != MarketDemandListing.Status.OPEN:
        raise MarketplaceStateError("Cannot respond — listing is no longer open.")
    if business.verification_status != Business.Status.VERIFIED:
        raise MarketplaceStateError("Business must be verified to respond to demand.")
    existing = DemandResponse.objects.filter(
        demand_listing=demand_listing,
        business=business,
        status__in=[DemandResponse.Status.SUBMITTED, DemandResponse.Status.SHORTLISTED],
    ).first()
    if existing:
        return existing
    response = DemandResponse.objects.create(
        demand_listing=demand_listing,
        entrepreneur=entrepreneur,
        business=business,
        product_listing=product_listing,
        message=message,
        pricing_offer=pricing_offer,
        attachment=attachment,
    )
    _audit(entrepreneur.user, AuditLog.Action.CREATE, response)
    mkt_signals.smehub_demand_response_submitted.send(
        sender=DemandResponse, response=response,
    )
    notify_smehub(
        demand_listing.buyer.user,
        f"New response from {business.name} on demand “{demand_listing.title}”.",
        verb=Verb.SMEHUB_DEMAND_RESPONSE,
        action_url=_safe_reverse("smehub_marketplace:demand_responses", pk=demand_listing.pk),
        target=response,
    )
    return response


@transaction.atomic
def decide_demand_response(
    response: DemandResponse,
    *,
    by_user,
    outcome: str,
    note: str = "",
) -> DemandResponse:
    valid = {
        DemandResponse.Status.SHORTLISTED,
        DemandResponse.Status.AWARDED,
        DemandResponse.Status.DECLINED,
    }
    if outcome not in valid:
        raise MarketplaceStateError("Outcome must be shortlisted / awarded / declined.")
    response.status = outcome
    response.decided_at = timezone.now()
    response.decided_by = by_user
    response.decision_note = note
    response.save(update_fields=["status", "decided_at", "decided_by", "decision_note", "updated_at"])
    _audit(by_user, AuditLog.Action.UPDATE, response, metadata={"outcome": outcome})
    mkt_signals.smehub_demand_response_decided.send(
        sender=DemandResponse, response=response, by_user=by_user, outcome=outcome,
    )
    if outcome == DemandResponse.Status.AWARDED:
        _create_sale_stub_from_award(response)
    notify_smehub(
        response.entrepreneur.user,
        f"Your response to “{response.demand_listing.title}” was {response.get_status_display().lower()}.",
        verb=Verb.SMEHUB_DEMAND_RESPONSE,
        target=response,
    )
    return response


# ---------------------------------------------------------------------------
# Market validation  (Phase 6.3)
# ---------------------------------------------------------------------------


@transaction.atomic
def validate_market_fit(
    *,
    product_listing: ProductListing,
    recorded_by,
    method: str,
    problem_fit_score: int,
    solution_fit_score: int,
    willingness_to_pay_score: int,
    conducted_at=None,
    validator_name: str = "",
    validator_email: str = "",
    validator_user=None,
    validator_buyer_entry: BuyerRegistryEntry | None = None,
    willingness_to_pay_amount=None,
    currency: str = "",
    notes: str = "",
    evidence_file=None,
) -> MarketValidation:
    """Capture a single customer-discovery touchpoint for a product listing."""
    if method not in dict(MarketValidation.Method.choices):
        raise MarketplaceStateError(f"Unknown validation method: {method}")
    if willingness_to_pay_amount is not None and not currency:
        raise MarketplaceStateError(
            "currency is required when willingness_to_pay_amount is provided."
        )
    if not _user_owns_business(recorded_by, product_listing.business):
        # Recorded_by must either own the business or be an SME admin.
        from apps.core.permissions.roles import UserRole as _Role

        if not (
            getattr(recorded_by, "is_superuser", False)
            or getattr(recorded_by, "role", "")
            in {_Role.ADMIN, _Role.SYSTEM_ADMIN, _Role.SME_ADMIN}
        ):
            raise MarketplaceStateError(
                "Only the owning entrepreneur or SME admin can record validations."
            )
    validation = MarketValidation.objects.create(
        product_listing=product_listing,
        business=product_listing.business,
        recorded_by=recorded_by,
        validator_name=validator_name,
        validator_email=validator_email,
        validator_user=validator_user,
        validator_buyer_entry=validator_buyer_entry,
        method=method,
        conducted_at=conducted_at or timezone.now(),
        problem_fit_score=int(problem_fit_score),
        solution_fit_score=int(solution_fit_score),
        willingness_to_pay_score=int(willingness_to_pay_score),
        willingness_to_pay_amount=willingness_to_pay_amount,
        currency=currency,
        notes=notes,
        evidence_file=evidence_file,
    )
    _audit(
        recorded_by,
        AuditLog.Action.CREATE,
        validation,
        metadata={"method": method, "average_score": str(validation.average_score)},
    )
    mkt_signals.smehub_market_validation_recorded.send(
        sender=MarketValidation, validation=validation, by_user=recorded_by,
    )
    owner_user = product_listing.entrepreneur.user
    if owner_user != recorded_by:
        notify_smehub(
            owner_user,
            f"New market validation recorded for '{product_listing.name}'.",
            verb=Verb.SMEHUB_MARKET_VALIDATION_RECORDED,
            target=validation,
        )
    return validation


def _user_owns_business(user, business: Business) -> bool:
    return getattr(business.entrepreneur, "user_id", None) == getattr(user, "pk", None)


# ---------------------------------------------------------------------------
# Sales record  (Phase 6.3)
# ---------------------------------------------------------------------------


@transaction.atomic
def record_sale(
    *,
    business: Business,
    entrepreneur,
    channel: str,
    quantity,
    gross_amount,
    currency: str,
    sale_date,
    product_listing: ProductListing | None = None,
    buyer_entry: BuyerRegistryEntry | None = None,
    customer_name: str = "",
    unit: str = "units",
    unit_price=None,
    notes: str = "",
    evidence_file=None,
    demand_response: DemandResponse | None = None,
    status: str = SalesRecord.Status.RECORDED,
) -> SalesRecord:
    """Record a booked sale. unit_price auto-derives if omitted."""
    if channel not in dict(SalesRecord.Channel.choices):
        raise MarketplaceStateError(f"Unknown sales channel: {channel}")
    if status not in dict(SalesRecord.Status.choices):
        raise MarketplaceStateError(f"Unknown sales status: {status}")
    if product_listing is not None and product_listing.business_id != business.pk:
        raise MarketplaceStateError("Product listing belongs to a different business.")
    if not buyer_entry and not customer_name:
        raise MarketplaceStateError("Provide either buyer_entry or customer_name.")
    sale = SalesRecord.objects.create(
        product_listing=product_listing,
        business=business,
        entrepreneur=entrepreneur,
        demand_response=demand_response,
        buyer_entry=buyer_entry,
        customer_name=customer_name,
        channel=channel,
        quantity=Decimal(quantity),
        unit=unit,
        unit_price=unit_price,
        gross_amount=Decimal(gross_amount),
        currency=currency,
        sale_date=sale_date,
        status=status,
        notes=notes,
        evidence_file=evidence_file,
    )
    _audit(
        entrepreneur,
        AuditLog.Action.CREATE,
        sale,
        metadata={"channel": channel, "amount": str(sale.gross_amount), "currency": currency},
    )
    mkt_signals.smehub_sale_recorded.send(
        sender=SalesRecord, sale=sale, by_user=entrepreneur,
    )
    return sale


def _create_sale_stub_from_award(response: DemandResponse) -> SalesRecord:
    """Auto-stub a SalesRecord in PENDING_DETAILS state for an awarded response.

    Idempotent on (demand_response). Notifies the entrepreneur to fill in
    quantity / amount / sale_date / evidence.
    """
    existing = SalesRecord.objects.filter(demand_response=response).first()
    if existing is not None:
        return existing
    sale = SalesRecord.objects.create(
        product_listing=response.product_listing,
        business=response.business,
        entrepreneur=response.entrepreneur.user,
        demand_response=response,
        buyer_entry=response.demand_listing.buyer,
        customer_name=response.demand_listing.buyer.organisation_name,
        channel=SalesRecord.Channel.MARKETPLACE_AWARD,
        quantity=Decimal("0.00"),
        gross_amount=Decimal("0.00"),
        currency=DEFAULT_CURRENCY,
        sale_date=None,
        status=SalesRecord.Status.PENDING_DETAILS,
        notes=f"Auto-created from awarded demand response #{response.pk}.",
    )
    _audit(
        response.decided_by,
        AuditLog.Action.CREATE,
        sale,
        metadata={"event": "sale_stub_from_award", "response_id": response.pk},
    )
    mkt_signals.smehub_sale_stub_created.send(
        sender=SalesRecord, sale=sale, response=response,
    )
    notify_smehub(
        response.entrepreneur.user,
        f"Complete the sale details for '{response.demand_listing.title}'.",
        verb=Verb.SMEHUB_SALE_STUB_CREATED,
        target=sale,
    )
    return sale


_SALE_UPDATABLE_FIELDS = {
    "product_listing",
    "buyer_entry",
    "customer_name",
    "channel",
    "quantity",
    "unit",
    "unit_price",
    "gross_amount",
    "currency",
    "sale_date",
    "notes",
    "evidence_file",
}


@transaction.atomic
def update_sale_record(sale: SalesRecord, *, by_user, **fields) -> SalesRecord:
    """Update a sale (entrepreneur owner or SME admin only)."""
    if sale.status in {SalesRecord.Status.VERIFIED, SalesRecord.Status.CANCELLED}:
        raise MarketplaceStateError(
            "Verified or cancelled sales cannot be edited; dispute or cancel instead."
        )
    changed: list[str] = []
    for key, value in fields.items():
        if key in _SALE_UPDATABLE_FIELDS and getattr(sale, key) != value:
            setattr(sale, key, value)
            changed.append(key)
    # Promote PENDING_DETAILS → RECORDED once the row is complete.
    if (
        sale.status == SalesRecord.Status.PENDING_DETAILS
        and sale.gross_amount
        and sale.gross_amount > 0
        and sale.sale_date is not None
    ):
        sale.status = SalesRecord.Status.RECORDED
        changed.append("status")
    if changed:
        if (
            "gross_amount" in changed
            or "quantity" in changed
            or "unit_price" in changed
        ):
            sale.unit_price = None  # let save() re-derive
        sale.save()
        _audit(
            by_user,
            AuditLog.Action.UPDATE,
            sale,
            metadata={"event": "sale_updated", "fields": changed},
        )
        mkt_signals.smehub_sale_updated.send(
            sender=SalesRecord, sale=sale, by_user=by_user, changed_fields=changed,
        )
    return sale


@transaction.atomic
def verify_sale(sale: SalesRecord, *, by_user) -> SalesRecord:
    if sale.status not in {SalesRecord.Status.PENDING_DETAILS, SalesRecord.Status.RECORDED}:
        raise MarketplaceStateError("Only PENDING_DETAILS or RECORDED sales can be verified.")
    if sale.status == SalesRecord.Status.PENDING_DETAILS and not sale.is_complete:
        raise MarketplaceStateError("Sale must be completed before verification.")
    sale.status = SalesRecord.Status.VERIFIED
    sale.verified_by = by_user
    sale.verified_at = timezone.now()
    sale.save(update_fields=["status", "verified_by", "verified_at", "updated_at"])
    _audit(by_user, AuditLog.Action.UPDATE, sale, metadata={"event": "sale_verified"})
    mkt_signals.smehub_sale_verified.send(sender=SalesRecord, sale=sale, by_user=by_user)
    notify_smehub(
        sale.entrepreneur,
        f"Your sale of {sale.gross_amount} {sale.currency} has been verified.",
        verb=Verb.SMEHUB_SALE_VERIFIED,
        target=sale,
    )
    return sale


@transaction.atomic
def dispute_sale(sale: SalesRecord, *, by_user, reason: str) -> SalesRecord:
    if sale.status not in {SalesRecord.Status.RECORDED, SalesRecord.Status.VERIFIED}:
        raise MarketplaceStateError("Only RECORDED or VERIFIED sales can be disputed.")
    if not reason.strip():
        raise MarketplaceStateError("Dispute reason is required.")
    sale.status = SalesRecord.Status.DISPUTED
    sale.dispute_reason = reason
    sale.save(update_fields=["status", "dispute_reason", "updated_at"])
    _audit(
        by_user,
        AuditLog.Action.UPDATE,
        sale,
        metadata={"event": "sale_disputed", "reason": reason},
    )
    mkt_signals.smehub_sale_disputed.send(
        sender=SalesRecord, sale=sale, by_user=by_user, reason=reason,
    )
    notify_smehub(
        sale.entrepreneur,
        f"Sale of {sale.gross_amount} {sale.currency} disputed. Reason: {reason}",
        verb=Verb.SMEHUB_SALE_DISPUTED,
        target=sale,
    )
    return sale


@transaction.atomic
def cancel_sale(sale: SalesRecord, *, by_user, reason: str = "") -> SalesRecord:
    if sale.status == SalesRecord.Status.CANCELLED:
        return sale
    sale.status = SalesRecord.Status.CANCELLED
    sale.cancel_reason = reason
    sale.save(update_fields=["status", "cancel_reason", "updated_at"])
    _audit(
        by_user,
        AuditLog.Action.UPDATE,
        sale,
        metadata={"event": "sale_cancelled", "reason": reason},
    )
    mkt_signals.smehub_sale_cancelled.send(
        sender=SalesRecord, sale=sale, by_user=by_user, reason=reason,
    )
    return sale
