"""SME-Hub linkage models — Subprocess SP3a.

Implements PRD Table 39 (FRSME-MPL001 → FRSME-MPL013, MPL017 → MPL019):
  • PartnerDirectoryEntry / ServiceProviderEntry (FRSME-MPL001–006)
  • ConnectionRequest + Connection (FRSME-MPL007–010)
  • MoU + MoUVersion + MoUComment (FRSME-MPL011–013)
  • AdvisorySession (FRSME-MPL017–019)
  • OrganisationRecommendation (PRD A2 — entrepreneur-suggested directory entry)
  • RecommendationSnapshot (FRSME-MPL005/006 cached AI matchmaking output)

FSMs live on PartnerDirectoryEntry / ServiceProviderEntry / ConnectionRequest
/ MoU. Never assign protected status fields directly — always call the
@transition method then save (project memory: FSM gotchas).
"""
from __future__ import annotations

import uuid

from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils import timezone
from django_fsm import FSMField, transition

from apps.core.storage.paths import upload_to_smehub_linkage
from apps.smehub.onboarding.models import EntrepreneurProfile


# ---------------------------------------------------------------------------
# Directories  (FRSME-MPL001 → MPL006)
# ---------------------------------------------------------------------------

class _DirectoryEntryBase(models.Model):
    class OrgType(models.TextChoices):
        GOVERNMENT = "government", "Government"
        CORPORATE = "corporate", "Corporate"
        NGO = "ngo", "NGO / non-profit"
        UNIVERSITY = "university", "University / research institute"
        FOUNDATION = "foundation", "Foundation"
        COOPERATIVE = "cooperative", "Cooperative"
        OTHER = "other", "Other"

    class Status(models.TextChoices):
        PENDING = "pending", "Pending verification"
        VERIFIED = "verified", "Verified"
        REJECTED = "rejected", "Rejected"
        DEACTIVATED = "deactivated", "Deactivated"

    organisation_name = models.CharField(max_length=255)
    org_type = models.CharField(max_length=32, choices=OrgType.choices, db_index=True)
    sector_focus = models.JSONField(
        default=list,
        blank=True,
        help_text="Sector tags (matches BUSINESS choices); FRSME-MPL004 search facet.",
    )
    geographic_coverage = models.JSONField(
        default=list,
        blank=True,
        help_text="ISO-2 country codes covered.",
    )
    contact_email = models.EmailField(blank=True)
    contact_phone = models.CharField(max_length=64, blank=True)
    website = models.URLField(blank=True)
    description = models.TextField(blank=True)
    areas_of_interest = models.TextField(
        blank=True,
        help_text="Free-text statement of partnership / sourcing interests.",
    )
    logo = models.FileField(upload_to=upload_to_smehub_linkage, null=True, blank=True)
    managed_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
        help_text="Account that maintains this entry (FRSME-MPL002 self-managed).",
    )
    verification_status = FSMField(
        max_length=16,
        choices=Status.choices,
        default=Status.PENDING,
        protected=True,
        db_index=True,
    )
    verified_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    verified_at = models.DateTimeField(null=True, blank=True)
    rejection_reason = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True
        ordering = ["organisation_name"]

    @transition(
        field=verification_status,
        source=[Status.PENDING, Status.REJECTED],
        target=Status.VERIFIED,
    )
    def verify(self, *, by_user) -> None:
        self.verified_by = by_user
        self.verified_at = timezone.now()
        self.rejection_reason = ""

    @transition(field=verification_status, source=Status.PENDING, target=Status.REJECTED)
    def reject(self, *, reason: str) -> None:
        self.rejection_reason = reason

    @transition(
        field=verification_status,
        source=[Status.VERIFIED, Status.PENDING, Status.REJECTED],
        target=Status.DEACTIVATED,
    )
    def deactivate(self, *, reason: str = "") -> None:
        """FRSME-MPL001 — admin can deactivate a directory entry."""
        if reason:
            self.rejection_reason = reason

    @transition(field=verification_status, source=Status.DEACTIVATED, target=Status.VERIFIED)
    def reactivate(self, *, by_user) -> None:
        self.verified_by = by_user
        self.verified_at = timezone.now()
        self.rejection_reason = ""


class PartnerDirectoryEntry(_DirectoryEntryBase):
    """Public directory of partner organisations (FRSME-MPL001–MPL006)."""

    class Meta(_DirectoryEntryBase.Meta):
        abstract = False
        verbose_name_plural = "Partner directory entries"
        indexes = [
            models.Index(fields=["org_type", "verification_status"]),
        ]

    def __str__(self) -> str:
        return self.organisation_name


class ServiceProviderEntry(_DirectoryEntryBase):
    """Service provider directory (PRD §3.2.6 service categories)."""

    class ServiceOffering(models.TextChoices):
        LEGAL = "legal", "Legal & compliance"
        FINANCE = "finance", "Finance & accounting"
        CERTIFICATION = "certification", "Certification & standards"
        PACKAGING = "packaging", "Packaging & branding"
        QA = "qa", "Quality assurance"
        IP = "ip", "Intellectual property"
        MARKETING = "marketing", "Marketing & communications"
        LOGISTICS = "logistics", "Logistics & distribution"
        TECH = "tech", "Technology & software"
        TRAINING = "training", "Training & capacity building"
        OTHER = "other", "Other"

    service_offerings = models.JSONField(
        default=list,
        blank=True,
        help_text="Subset of ServiceOffering values (multi-select).",
    )
    available_slots = models.JSONField(
        default=dict,
        blank=True,
        help_text="Free-form schedule payload — keyed advisory booking windows.",
    )

    class Meta(_DirectoryEntryBase.Meta):
        abstract = False
        verbose_name_plural = "Service provider entries"
        indexes = [
            models.Index(fields=["org_type", "verification_status"]),
        ]

    def __str__(self) -> str:
        return self.organisation_name


# ---------------------------------------------------------------------------
# Connections  (FRSME-MPL007 → MPL010)
# ---------------------------------------------------------------------------

class ConnectionRequest(models.Model):
    """Entrepreneur-initiated request to a partner / buyer / service provider.

    FRSME-MPL007–010. The target is polymorphic via GenericForeignKey to
    avoid a forest of nullable FKs across SP3 + SP3b modules.
    """

    class Status(models.TextChoices):
        PENDING = "pending", "Pending"
        ACCEPTED = "accepted", "Accepted"
        DECLINED = "declined", "Declined"
        WITHDRAWN = "withdrawn", "Withdrawn"

    entrepreneur = models.ForeignKey(
        EntrepreneurProfile,
        on_delete=models.CASCADE,
        related_name="connection_requests",
    )
    business = models.ForeignKey(
        "smehub_onboarding.Business",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="connection_requests",
        help_text="Business profile attached to the request (FRSME-MPL007).",
    )
    target_content_type = models.ForeignKey(
        ContentType,
        on_delete=models.CASCADE,
        related_name="+",
    )
    target_object_id = models.CharField(max_length=64)
    target = GenericForeignKey("target_content_type", "target_object_id")
    message = models.TextField(blank=True)
    status = FSMField(
        max_length=16,
        choices=Status.choices,
        default=Status.PENDING,
        protected=True,
        db_index=True,
    )
    decline_reason = models.TextField(blank=True)
    decided_at = models.DateTimeField(null=True, blank=True)
    decided_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["-created_at"]
        constraints = [
            # FRSME-MPL008: prevent duplicate pending OR active request to the same target
            models.UniqueConstraint(
                fields=["entrepreneur", "target_content_type", "target_object_id"],
                condition=models.Q(status__in=["pending", "accepted"]),
                name="uniq_smehub_active_connection_request",
            ),
        ]
        indexes = [
            models.Index(fields=["target_content_type", "target_object_id", "status"]),
            models.Index(fields=["entrepreneur", "status"]),
        ]

    def __str__(self) -> str:
        return f"{self.entrepreneur.user.email} → {self.target} ({self.status})"

    @transition(field=status, source=Status.PENDING, target=Status.ACCEPTED)
    def accept(self, *, by_user) -> None:
        self.decided_at = timezone.now()
        self.decided_by = by_user
        self.decline_reason = ""

    @transition(field=status, source=Status.PENDING, target=Status.DECLINED)
    def decline(self, *, by_user, reason: str = "") -> None:
        self.decided_at = timezone.now()
        self.decided_by = by_user
        self.decline_reason = reason

    @transition(field=status, source=Status.PENDING, target=Status.WITHDRAWN)
    def withdraw(self) -> None:
        pass


class Connection(models.Model):
    """Open relationship created on accept (FRSME-MPL010). Holds a single
    accepted request and surfaces both-party shortcuts for templates.
    """

    class Status(models.TextChoices):
        ACTIVE = "active", "Active"
        ENDED = "ended", "Ended"

    request = models.OneToOneField(
        ConnectionRequest,
        on_delete=models.CASCADE,
        related_name="connection",
    )
    entrepreneur = models.ForeignKey(
        EntrepreneurProfile,
        on_delete=models.CASCADE,
        related_name="connections",
    )
    target_content_type = models.ForeignKey(
        ContentType,
        on_delete=models.CASCADE,
        related_name="+",
    )
    target_object_id = models.CharField(max_length=64)
    target = GenericForeignKey("target_content_type", "target_object_id")
    status = models.CharField(
        max_length=16,
        choices=Status.choices,
        default=Status.ACTIVE,
        db_index=True,
    )
    ended_at = models.DateTimeField(null=True, blank=True)
    ended_reason = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["-created_at"]
        indexes = [
            models.Index(fields=["entrepreneur", "status"]),
            models.Index(fields=["target_content_type", "target_object_id", "status"]),
        ]

    def __str__(self) -> str:
        return f"Connection {self.entrepreneur.user.email} ↔ {self.target}"


# ---------------------------------------------------------------------------
# MoUs  (FRSME-MPL011 → MPL013, A5)
# ---------------------------------------------------------------------------

class MoU(models.Model):
    """Memorandum of Understanding tied to a Connection.

    FRSME-MPL011–013 + acceptance A5 (versioned amendments). The current
    draft / signed file lives on this row; historic versions live in the
    `MoUVersion` table.
    """

    class Status(models.TextChoices):
        DRAFT = "draft", "Draft"
        UNDER_REVIEW = "under_review", "Under review"
        FORMALISED = "formalised", "Formalised"
        AMENDED = "amended", "Amended"
        TERMINATED = "terminated", "Terminated"

    connection = models.ForeignKey(
        Connection,
        on_delete=models.CASCADE,
        related_name="mous",
    )
    title = models.CharField(max_length=255)
    summary = models.TextField(blank=True)
    draft_file = models.FileField(
        upload_to=upload_to_smehub_linkage,
        null=True,
        blank=True,
    )
    signed_file = models.FileField(
        upload_to=upload_to_smehub_linkage,
        null=True,
        blank=True,
    )
    status = FSMField(
        max_length=16,
        choices=Status.choices,
        default=Status.DRAFT,
        protected=True,
        db_index=True,
    )
    formalised_at = models.DateTimeField(null=True, blank=True)
    formalised_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    parent_mou = models.ForeignKey(
        "self",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="amendments",
        help_text="Set when this MoU is an amendment of an earlier formalised MoU (A5).",
    )
    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["-created_at"]

    def __str__(self) -> str:
        return f"MoU: {self.title}"

    @transition(field=status, source=Status.DRAFT, target=Status.UNDER_REVIEW)
    def submit_for_review(self) -> None:
        pass

    @transition(
        field=status,
        source=[Status.DRAFT, Status.UNDER_REVIEW],
        target=Status.FORMALISED,
    )
    def formalise(self, *, by_user) -> None:
        self.formalised_at = timezone.now()
        self.formalised_by = by_user

    @transition(field=status, source=Status.FORMALISED, target=Status.AMENDED)
    def mark_amended(self) -> None:
        pass

    @transition(
        field=status,
        source=[Status.FORMALISED, Status.AMENDED],
        target=Status.TERMINATED,
    )
    def terminate(self) -> None:
        pass


class MoUPartyConfirmation(models.Model):
    """FRSME-MPL013 audit fix — explicit two-party sign-off before an MoU
    can be formalised. Each party (entrepreneur + counterparty owner)
    creates one row to record their acceptance; ``formalise_mou`` refuses
    to flip the MoU status until both rows exist.
    """

    class Party(models.TextChoices):
        ENTREPRENEUR = "entrepreneur", "Entrepreneur"
        COUNTERPARTY = "counterparty", "Counterparty (partner / SP / buyer)"

    mou = models.ForeignKey(MoU, on_delete=models.CASCADE, related_name="party_confirmations")
    party = models.CharField(max_length=16, choices=Party.choices)
    confirmed_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
    )
    confirmed_at = models.DateTimeField(default=timezone.now)
    note = models.CharField(max_length=255, blank=True)

    class Meta:
        ordering = ["mou", "party"]
        constraints = [
            models.UniqueConstraint(fields=["mou", "party"], name="uniq_smehub_mou_party_confirmation"),
        ]

    def __str__(self) -> str:
        return f"{self.get_party_display()} confirmation for MoU #{self.mou_id}"


class MoUVersion(models.Model):
    """Frozen snapshot of an MoU at a given revision (FRSME-MPL013, A5)."""

    mou = models.ForeignKey(MoU, on_delete=models.CASCADE, related_name="versions")
    version_number = models.PositiveSmallIntegerField()
    title = models.CharField(max_length=255)
    summary = models.TextField(blank=True)
    file = models.FileField(upload_to=upload_to_smehub_linkage, null=True, blank=True)
    status_at_version = models.CharField(max_length=16)
    notes = models.TextField(blank=True)
    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
    )
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-version_number"]
        constraints = [
            models.UniqueConstraint(
                fields=["mou", "version_number"],
                name="uniq_smehub_mou_version",
            ),
        ]

    def __str__(self) -> str:
        return f"{self.mou.title} v{self.version_number}"


class MoUComment(models.Model):
    """Threaded comment on an MoU draft (FRSME-MPL012)."""

    mou = models.ForeignKey(MoU, on_delete=models.CASCADE, related_name="comments")
    parent = models.ForeignKey(
        "self",
        null=True,
        blank=True,
        on_delete=models.CASCADE,
        related_name="replies",
    )
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.PROTECT,
        related_name="+",
    )
    body = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["created_at"]

    def __str__(self) -> str:
        return f"Comment by {self.author.email} on {self.mou.title}"


# ---------------------------------------------------------------------------
# Advisory bookings  (FRSME-MPL017 → MPL019)
# ---------------------------------------------------------------------------

class AdvisorySession(models.Model):
    class Type(models.TextChoices):
        VIRTUAL = "virtual", "Virtual"
        PHYSICAL = "physical", "Physical"

    class Status(models.TextChoices):
        SCHEDULED = "scheduled", "Scheduled"
        COMPLETED = "completed", "Completed"
        CANCELLED = "cancelled", "Cancelled"
        NO_SHOW = "no_show", "No show"

    entrepreneur = models.ForeignKey(
        EntrepreneurProfile,
        on_delete=models.CASCADE,
        related_name="advisory_sessions",
    )
    service_provider = models.ForeignKey(
        ServiceProviderEntry,
        on_delete=models.CASCADE,
        related_name="advisory_sessions",
    )
    type = models.CharField(max_length=16, choices=Type.choices, default=Type.VIRTUAL)
    starts_at = models.DateTimeField()
    duration_minutes = models.PositiveSmallIntegerField(default=60)
    location = models.CharField(max_length=255, blank=True)
    agenda = models.TextField(blank=True)
    notes = models.TextField(blank=True)
    status = models.CharField(
        max_length=16,
        choices=Status.choices,
        default=Status.SCHEDULED,
        db_index=True,
    )
    completed_at = models.DateTimeField(null=True, blank=True)
    cancellation_reason = models.CharField(max_length=255, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["starts_at"]
        indexes = [
            models.Index(fields=["entrepreneur", "status"]),
            models.Index(fields=["service_provider", "status"]),
        ]

    def __str__(self) -> str:
        return f"Advisory {self.starts_at:%Y-%m-%d %H:%M} — {self.service_provider.organisation_name}"


class AdvisorySlot(models.Model):
    """FRSME-MPL017 — a bookable advisory time slot published by a service
    provider. An entrepreneur books a session by picking an open slot; the slot
    is consumed on booking (``session`` set) and freed again if that session is
    cancelled, so the same window can never be double-booked or booked
    off-hours.
    """

    service_provider = models.ForeignKey(
        ServiceProviderEntry,
        on_delete=models.CASCADE,
        related_name="advisory_slots",
    )
    starts_at = models.DateTimeField()
    duration_minutes = models.PositiveSmallIntegerField(default=60)
    # Set to the booked session; NULL means the slot is open. SET_NULL frees the
    # slot automatically if the session row is ever deleted.
    session = models.OneToOneField(
        "AdvisorySession",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="slot",
    )
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["starts_at"]
        constraints = [
            models.UniqueConstraint(
                fields=["service_provider", "starts_at"],
                name="uniq_advisory_slot_per_provider_time",
            ),
        ]
        indexes = [
            models.Index(fields=["service_provider", "starts_at"]),
        ]

    def __str__(self) -> str:
        return f"Slot {self.starts_at:%Y-%m-%d %H:%M} — {self.service_provider.organisation_name}"

    @property
    def is_booked(self) -> bool:
        return self.session_id is not None


# ---------------------------------------------------------------------------
# Suggestions + AI snapshots
# ---------------------------------------------------------------------------

class OrganisationRecommendation(models.Model):
    """Entrepreneur-submitted suggestion for a new directory entry (PRD A2)."""

    class Kind(models.TextChoices):
        PARTNER = "partner", "Partner"
        SERVICE_PROVIDER = "service_provider", "Service provider"
        BUYER = "buyer", "Buyer"

    class Status(models.TextChoices):
        SUBMITTED = "submitted", "Submitted"
        UNDER_REVIEW = "under_review", "Under review"
        ADOPTED = "adopted", "Adopted"
        DISMISSED = "dismissed", "Dismissed"

    suggested_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
    )
    kind = models.CharField(max_length=24, choices=Kind.choices)
    organisation_name = models.CharField(max_length=255)
    contact_email = models.EmailField(blank=True)
    contact_phone = models.CharField(max_length=64, blank=True)
    sector_focus = models.JSONField(default=list, blank=True)
    geographic_coverage = models.JSONField(default=list, blank=True)
    rationale = models.TextField(blank=True)
    status = models.CharField(
        max_length=16,
        choices=Status.choices,
        default=Status.SUBMITTED,
        db_index=True,
    )
    handled_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
    )
    handled_at = models.DateTimeField(null=True, blank=True)
    handler_note = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at"]

    def __str__(self) -> str:
        return f"{self.get_kind_display()} suggestion: {self.organisation_name}"


class RecommendationSnapshot(models.Model):
    """Cached AI matchmaking output (FRSME-MPL005, MPL006).

    `recommendations` is a list of dicts containing entry_id, kind, score,
    reason — enough for the dashboard to render without re-querying the AI
    backend on every page view.
    """

    entrepreneur = models.OneToOneField(
        EntrepreneurProfile,
        on_delete=models.CASCADE,
        related_name="recommendation_snapshot",
    )
    snapshot_id = models.UUIDField(default=uuid.uuid4, editable=False)
    recommendations = models.JSONField(default=list, blank=True)
    based_on = models.JSONField(
        default=dict,
        blank=True,
        help_text="Inputs hash: business sectors / stages / geo / programmes.",
    )
    refreshed_at = models.DateTimeField(default=timezone.now)
    refreshed_by_signal = models.CharField(max_length=64, blank=True)
    is_fallback = models.BooleanField(
        default=False,
        help_text="True when the AI backend was unavailable and a deterministic ranker was used.",
    )

    class Meta:
        ordering = ["-refreshed_at"]

    def __str__(self) -> str:
        return f"Snapshot for {self.entrepreneur.user.email} @ {self.refreshed_at:%Y-%m-%d %H:%M}"


# ---------------------------------------------------------------------------
# Regulatory & Policy Navigation  (PRD §5.X.X — Regulatory and Policy)
# ---------------------------------------------------------------------------

class RegulatoryGuide(models.Model):
    """Curated knowledge-base entry for regulatory / certification / licensing
    pathways relevant to SME-Hub innovators.

    Entrepreneurs filter by sector + country to find applicable standards
    and approval pathways. Maintained by admins; PRD prose feature
    "Regulatory and Policy Navigation".
    """

    class BodyType(models.TextChoices):
        STANDARD = "standard", "Standard / specification"
        APPROVAL = "approval", "Regulatory approval"
        LICENSING = "licensing", "Licensing pathway"
        CERTIFICATION = "certification", "Certification scheme"
        POLICY = "policy", "Policy / framework"
        OTHER = "other", "Other"

    title = models.CharField(max_length=255)
    slug = models.SlugField(max_length=128, unique=True)
    body_type = models.CharField(
        max_length=24,
        choices=BodyType.choices,
        default=BodyType.STANDARD,
        db_index=True,
    )
    sectors = models.JSONField(
        default=list,
        blank=True,
        help_text="List of sector tokens this guide applies to (e.g. ['agriculture','health']).",
    )
    countries = models.JSONField(
        default=list,
        blank=True,
        help_text="ISO-3166 alpha-2 country codes; empty list = applies regionally.",
    )
    summary = models.TextField()
    issuing_authority = models.CharField(max_length=255, blank=True)
    official_link = models.URLField(blank=True)
    last_reviewed_at = models.DateTimeField(null=True, blank=True)
    is_active = models.BooleanField(default=True, db_index=True)
    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="smehub_regulatory_guides_authored",
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["title"]
        indexes = [
            models.Index(fields=["is_active", "body_type"]),
        ]

    def __str__(self) -> str:
        return self.title
