"""SME-Hub showcasing models — Subprocess SP4.

Implements PRD Tables 41–42 (FRSME-ISD001 → FRSME-ISD020):
  • ShowcaseEvent          — virtual / physical / hybrid showcase (FRSME-ISD001, 003, 011)
  • InnovationChallenge    — sector / theme challenge (FRSME-ISD002, 008)
  • ChallengeScoringRubric / ChallengeRubricSection — judging rubric (mirror SP2)
  • ShowcaseApplication    — entrepreneur applies to event/challenge (FRSME-ISD004–006)
  • InnovationCatalogueEntry — published innovation profile (FRSME-ISD009–010)
  • InnovationCatalogueVersion — frozen revision history
  • EventAttendance        — attendance check-in (FRSME-ISD012)
  • PitchScore             — judge scores against the rubric (FRSME-ISD013)
  • DealRoom               — secure space (FRSME-ISD014–016)
  • DealRoomMessage / DealRoomDocument — message + doc artefacts
  • DealAgreement          — formalised outcome (FRSME-ISD019)

FSMs live on ShowcaseEvent / ShowcaseApplication / DealRoom. Never assign
protected status fields directly — call the @transition method, then save().
After a transition, never call `refresh_from_db()` — fetch the row anew with
`Model.objects.get(pk=...)` (project memory: FSM gotchas).
"""
from __future__ import annotations

import uuid
from decimal import Decimal

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_showcasing
from apps.smehub.onboarding.models import Business, EntrepreneurProfile


# ---------------------------------------------------------------------------
# Showcase events  (FRSME-ISD001, 003, 011)
# ---------------------------------------------------------------------------

class ShowcaseEvent(models.Model):
    """Public showcase event hosted by the platform.

    FRSME-ISD001 — admins schedule a showcase with format, date, target sectors,
    eligibility rules, evaluation panel, and an optional video link for
    livestream embed (FRSME-ISD003 / ISD011).
    """

    class Format(models.TextChoices):
        VIRTUAL = "virtual", "Virtual"
        PHYSICAL = "physical", "Physical"
        HYBRID = "hybrid", "Hybrid"

    class Status(models.TextChoices):
        DRAFT = "draft", "Draft"
        PUBLISHED = "published", "Published"
        LIVE = "live", "Live"
        CONCLUDED = "concluded", "Concluded"

    name = models.CharField(max_length=255)
    summary = models.TextField(blank=True)
    description = models.TextField(blank=True)
    format = models.CharField(max_length=16, choices=Format.choices, default=Format.VIRTUAL)
    starts_at = models.DateTimeField()
    ends_at = models.DateTimeField()
    application_deadline = models.DateTimeField(null=True, blank=True)
    target_sectors = models.JSONField(
        default=list,
        blank=True,
        help_text="Sector tags (matches BUSINESS choices); FRSME-ISD002 facet.",
    )
    eligibility = models.JSONField(
        default=dict,
        blank=True,
        help_text="Free-form eligibility payload (stages, geography, programmes).",
    )
    venue = models.CharField(max_length=255, blank=True)
    video_link = models.URLField(blank=True, help_text="Livestream / recording URL (FRSME-ISD011).")
    # FRSME-ISD011 — generated video-conferencing session. The meeting id +
    # passwords are minted on go-live for virtual/hybrid events; confirmed
    # participants and evaluators reach it through the gated in-app join view,
    # which records access in ``ShowcaseEventAccess``.
    conference_meeting_id = models.CharField(max_length=80, blank=True)
    conference_attendee_pw = models.CharField(max_length=40, blank=True)
    conference_moderator_pw = models.CharField(max_length=40, blank=True)
    cover_image = models.FileField(upload_to=upload_to_smehub_showcasing, null=True, blank=True)
    evaluation_panel = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        blank=True,
        related_name="smehub_showcase_panels",
        help_text="Users with the JUDGE role assigned to evaluate pitches.",
    )
    organiser = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="smehub_showcases_organised",
    )
    status = FSMField(
        max_length=16,
        choices=Status.choices,
        default=Status.DRAFT,
        protected=True,
        db_index=True,
    )
    published_at = models.DateTimeField(null=True, blank=True)
    concluded_at = models.DateTimeField(null=True, 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=["status", "starts_at"]),
        ]

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

    @transition(field=status, source=Status.DRAFT, target=Status.PUBLISHED)
    def publish(self) -> None:
        self.published_at = timezone.now()

    @transition(field=status, source=Status.PUBLISHED, target=Status.DRAFT)
    def revert_to_draft(self) -> None:
        self.published_at = None

    @transition(field=status, source=Status.PUBLISHED, target=Status.LIVE)
    def go_live(self) -> None:
        pass

    @transition(field=status, source=[Status.PUBLISHED, Status.LIVE], target=Status.CONCLUDED)
    def conclude(self) -> None:
        self.concluded_at = timezone.now()

    @property
    def is_virtual_or_hybrid(self) -> bool:
        return self.format in (self.Format.VIRTUAL, self.Format.HYBRID)


class ShowcaseEventAccess(models.Model):
    """FRSME-ISD011 — audit row written each time a confirmed participant or
    evaluator opens the generated conferencing link for a showcase event."""

    class Role(models.TextChoices):
        PARTICIPANT = "participant", "Participant"
        EVALUATOR = "evaluator", "Evaluator"
        ORGANISER = "organiser", "Organiser"

    event = models.ForeignKey(
        ShowcaseEvent,
        on_delete=models.CASCADE,
        related_name="access_log",
    )
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="+",
    )
    role = models.CharField(max_length=16, choices=Role.choices)
    accessed_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-accessed_at"]
        indexes = [
            models.Index(fields=["event", "-accessed_at"]),
        ]

    def __str__(self) -> str:
        return f"{self.user} → {self.event} @ {self.accessed_at:%Y-%m-%d %H:%M}"


# ---------------------------------------------------------------------------
# Innovation challenges  (FRSME-ISD002, 008)
# ---------------------------------------------------------------------------

class InnovationChallenge(models.Model):
    """Sector / theme challenge that solicits innovation submissions.

    Mirrors SP2's rubric pattern (separate ChallengeScoringRubric model) so
    coupling stays low even though the math is identical to incubation.
    """

    class Status(models.TextChoices):
        DRAFT = "draft", "Draft"
        OPEN = "open", "Open"
        JUDGING = "judging", "Judging"
        ANNOUNCED = "announced", "Winners announced"
        CLOSED = "closed", "Closed"

    title = models.CharField(max_length=255)
    summary = models.TextField(blank=True)
    description = models.TextField(blank=True)
    theme = models.CharField(max_length=255, blank=True)
    target_sectors = models.JSONField(default=list, blank=True)
    eligibility = models.JSONField(default=dict, blank=True)
    prize_summary = models.TextField(blank=True)
    application_deadline = models.DateTimeField()
    judging_deadline = models.DateTimeField(null=True, blank=True)
    status = models.CharField(
        max_length=16,
        choices=Status.choices,
        default=Status.DRAFT,
        db_index=True,
    )
    organiser = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="smehub_challenges_organised",
    )
    # FRSME-ISD008 — challenge-level evaluation panel. Audit found that
    # ``PitchScoringView._is_assigned`` accepted any user with the JUDGE
    # role for any challenge ("any judge can score in this MVP" comment).
    # Mirroring ShowcaseEvent's evaluation_panel closes the safety gap
    # without introducing a per-application JudgeAssignment table.
    evaluation_panel = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        blank=True,
        related_name="smehub_challenge_panels",
        help_text="Users with the JUDGE role assigned to score this challenge.",
    )
    cover_image = models.FileField(upload_to=upload_to_smehub_showcasing, null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    published_at = models.DateTimeField(null=True, blank=True)
    announced_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        ordering = ["-application_deadline"]
        indexes = [models.Index(fields=["status", "application_deadline"])]

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


class ChallengeScoringRubric(models.Model):
    """One rubric per challenge. Sections sum to weight 1.0 (normalised)."""

    challenge = models.OneToOneField(
        InnovationChallenge,
        on_delete=models.CASCADE,
        related_name="rubric",
    )
    description = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

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

    @property
    def total_weight(self) -> Decimal:
        return self.sections.aggregate(total=models.Sum("weight"))["total"] or Decimal("0")

    @property
    def is_complete(self) -> bool:
        return self.sections.exists() and abs(self.total_weight - Decimal("1")) < Decimal("0.001")


class ChallengeRubricCategory(models.Model):
    """A weighted grouping of rubric criteria (e.g. the ESG innovation rubric's
    "Scientific Foundation and Innovation (20 points)").

    Categories are a *display + grouping* layer above the scored criteria.
    They carry no independent scoring weight of their own — a category's point
    total is simply the sum of its criteria's ``max_marks`` — but storing the
    headline ``points`` keeps the rubric readable and lets us render the
    category banner without re-deriving it every time.
    """

    rubric = models.ForeignKey(
        ChallengeScoringRubric,
        on_delete=models.CASCADE,
        related_name="categories",
    )
    title = models.CharField(max_length=255)
    description = models.TextField(blank=True)
    points = models.PositiveSmallIntegerField(
        default=0,
        help_text="Headline point total for the category (sum of its criteria).",
    )
    order = models.PositiveSmallIntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["order", "id"]
        verbose_name_plural = "challenge rubric categories"

    def __str__(self) -> str:  # pragma: no cover - admin display only
        return self.title


class ChallengeRubricSection(models.Model):
    """A single scored criterion of a challenge rubric.

    Despite the legacy name "section", this is the atomic *criterion* a judge
    scores (e.g. "Scientific Rigor", out of 5). Criteria may optionally belong
    to a :class:`ChallengeRubricCategory` for grouped display; ungrouped
    criteria (``category=None``) render as a flat list, preserving the pre-ESG
    behaviour.
    """

    rubric = models.ForeignKey(
        ChallengeScoringRubric,
        on_delete=models.CASCADE,
        related_name="sections",
    )
    category = models.ForeignKey(
        ChallengeRubricCategory,
        on_delete=models.SET_NULL,
        related_name="criteria",
        null=True,
        blank=True,
    )
    title = models.CharField(max_length=255)
    instructions = models.TextField(blank=True)
    weight = models.DecimalField(
        max_digits=5,
        decimal_places=4,
        default=Decimal("0.25"),
        help_text="Fractional weight (sections must sum to 1.0).",
    )
    max_marks = models.PositiveSmallIntegerField(default=10)
    # Ordered score-band descriptions keyed by the score value, e.g.
    # {"5": "Solution based on solid scientific principles…", "4": "…"}.
    # Purely descriptive guidance for judges; scoring never reads these.
    score_anchors = models.JSONField(default=dict, blank=True)
    order = models.PositiveSmallIntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["order", "id"]

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

    @property
    def anchor_rows(self) -> list[tuple[int, str]]:
        """``score_anchors`` as descending ``(value, description)`` rows for
        template rendering."""
        rows = []
        for key, desc in (self.score_anchors or {}).items():
            try:
                rows.append((int(key), desc))
            except (TypeError, ValueError):
                continue
        return sorted(rows, key=lambda r: r[0], reverse=True)


# ---------------------------------------------------------------------------
# Showcase application  (FRSME-ISD004–006)
# ---------------------------------------------------------------------------

class ShowcaseApplication(models.Model):
    """Entrepreneur application to a ShowcaseEvent OR an InnovationChallenge.

    The target is polymorphic via GenericForeignKey to keep one application
    table for both types — uniqueness is enforced per (entrepreneur, target).
    """

    class Status(models.TextChoices):
        DRAFT = "draft", "Draft"
        SUBMITTED = "submitted", "Submitted"
        UNDER_REVIEW = "under_review", "Under review"
        CONFIRMED = "confirmed", "Confirmed"
        REJECTED = "rejected", "Rejected"
        WITHDRAWN = "withdrawn", "Withdrawn"

    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")

    entrepreneur = models.ForeignKey(
        EntrepreneurProfile,
        on_delete=models.PROTECT,
        related_name="smehub_showcase_applications",
    )
    business = models.ForeignKey(
        Business,
        on_delete=models.PROTECT,
        related_name="smehub_showcase_applications",
    )
    application_id = models.CharField(
        max_length=20,
        unique=True,
        editable=False,
    )
    innovation_title = models.CharField(max_length=255)
    innovation_description = models.TextField(blank=True)
    problem_statement = models.TextField(blank=True)
    solution_summary = models.TextField(blank=True)
    impact_potential = models.TextField(blank=True)
    pitch_deck = models.FileField(upload_to=upload_to_smehub_showcasing, null=True, blank=True)
    pitch_video_link = models.URLField(blank=True)
    supporting_link = models.URLField(blank=True)
    status = FSMField(
        max_length=16,
        choices=Status.choices,
        default=Status.SUBMITTED,
        protected=True,
        db_index=True,
    )
    submitted_at = models.DateTimeField(default=timezone.now)
    decided_at = models.DateTimeField(null=True, blank=True)
    decision_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    decision_note = models.TextField(blank=True)
    weighted_score = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True)
    # FRSME-ISD A5 — challenge-winner flag set by ``announce_winners``.
    # Surfaces on the entrepreneur's profile / catalogue listing as a badge.
    is_challenge_winner = models.BooleanField(default=False, db_index=True)
    challenge_winner_notes = models.CharField(max_length=255, blank=True)
    challenge_winner_at = models.DateTimeField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["-created_at"]
        constraints = [
            models.UniqueConstraint(
                fields=["entrepreneur", "target_content_type", "target_object_id"],
                condition=models.Q(status__in=["submitted", "under_review", "confirmed"]),
                name="uniq_smehub_active_showcase_application",
            ),
        ]
        indexes = [
            models.Index(fields=["target_content_type", "target_object_id", "status"]),
            models.Index(fields=["entrepreneur", "status"]),
        ]

    def __str__(self) -> str:
        return f"{self.application_id} — {self.innovation_title}"

    def save(self, *args, **kwargs):
        if not self.application_id:
            self.application_id = f"SHC-{uuid.uuid4().hex[:8].upper()}"
        super().save(*args, **kwargs)

    @transition(field=status, source=Status.DRAFT, target=Status.SUBMITTED)
    def submit(self) -> None:
        """FRSME-ISD004 — move a saved draft to Submitted."""
        self.submitted_at = timezone.now()

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

    @transition(
        field=status,
        source=[Status.SUBMITTED, Status.UNDER_REVIEW],
        target=Status.CONFIRMED,
    )
    def confirm(self, *, by_user, note: str = "", weighted_score: Decimal | None = None) -> None:
        self.decision_by = by_user
        self.decided_at = timezone.now()
        self.decision_note = note
        if weighted_score is not None:
            self.weighted_score = weighted_score

    @transition(
        field=status,
        source=[Status.SUBMITTED, Status.UNDER_REVIEW],
        target=Status.REJECTED,
    )
    def reject(self, *, by_user, note: str = "", weighted_score: Decimal | None = None) -> None:
        self.decision_by = by_user
        self.decided_at = timezone.now()
        self.decision_note = note
        if weighted_score is not None:
            self.weighted_score = weighted_score

    @transition(
        field=status,
        source=[Status.SUBMITTED, Status.UNDER_REVIEW, Status.CONFIRMED],
        target=Status.WITHDRAWN,
    )
    def withdraw(self) -> None:
        pass

    # Convenience -------------------------------------------------------
    @property
    def is_event_application(self) -> bool:
        return self.target_content_type.model == "showcaseevent"

    @property
    def is_challenge_application(self) -> bool:
        return self.target_content_type.model == "innovationchallenge"


# ---------------------------------------------------------------------------
# Innovation catalogue  (FRSME-ISD009–010)
# ---------------------------------------------------------------------------

class InnovationCatalogueEntry(models.Model):
    """Public innovation profile derived from a confirmed ShowcaseApplication.

    Visible to investors, partners, and buyers. Updatable — each save snapshots
    the prior version (FRSME-ISD010 acceptance: history preserved).
    """

    class Visibility(models.TextChoices):
        PUBLIC = "public", "Public — visible to all logged-in stakeholders"
        STAKEHOLDER = "stakeholder", "Stakeholder — investors / partners / buyers only"
        PRIVATE = "private", "Private — admin only"

    application = models.OneToOneField(
        ShowcaseApplication,
        on_delete=models.CASCADE,
        related_name="catalogue_entry",
    )
    entrepreneur = models.ForeignKey(
        EntrepreneurProfile,
        on_delete=models.PROTECT,
        related_name="smehub_catalogue_entries",
    )
    business = models.ForeignKey(
        Business,
        on_delete=models.PROTECT,
        related_name="smehub_catalogue_entries",
    )
    title = models.CharField(max_length=255)
    summary = models.TextField(blank=True)
    description = models.TextField(blank=True)
    sectors = models.JSONField(default=list, blank=True)
    impact_metrics = models.JSONField(default=dict, blank=True)
    cover_image = models.FileField(upload_to=upload_to_smehub_showcasing, null=True, blank=True)
    pitch_deck = models.FileField(upload_to=upload_to_smehub_showcasing, null=True, blank=True)
    video_link = models.URLField(blank=True)
    website = models.URLField(blank=True)
    visibility = models.CharField(
        max_length=16,
        choices=Visibility.choices,
        default=Visibility.STAKEHOLDER,
        db_index=True,
    )
    is_active = models.BooleanField(default=True)
    version = models.PositiveSmallIntegerField(default=1)
    published_at = models.DateTimeField(default=timezone.now)
    last_updated_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["-published_at"]
        verbose_name_plural = "Innovation catalogue entries"

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


class InnovationCatalogueVersion(models.Model):
    """Frozen snapshot of a catalogue entry at a given version (FRSME-ISD010 history)."""

    entry = models.ForeignKey(
        InnovationCatalogueEntry,
        on_delete=models.CASCADE,
        related_name="versions",
    )
    version = models.PositiveSmallIntegerField()
    title = models.CharField(max_length=255)
    summary = models.TextField(blank=True)
    description = models.TextField(blank=True)
    sectors = models.JSONField(default=list, blank=True)
    impact_metrics = models.JSONField(default=dict, blank=True)
    cover_image = models.FileField(upload_to=upload_to_smehub_showcasing, null=True, blank=True)
    pitch_deck = models.FileField(upload_to=upload_to_smehub_showcasing, null=True, blank=True)
    video_link = models.URLField(blank=True)
    website = models.URLField(blank=True)
    notes = models.TextField(blank=True)
    captured_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    captured_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-version"]
        constraints = [
            models.UniqueConstraint(
                fields=["entry", "version"],
                name="uniq_smehub_catalogue_version",
            ),
        ]

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


# ---------------------------------------------------------------------------
# Attendance + scoring  (FRSME-ISD012, ISD013)
# ---------------------------------------------------------------------------

class EventAttendance(models.Model):
    event = models.ForeignKey(
        ShowcaseEvent,
        on_delete=models.CASCADE,
        related_name="attendance_records",
    )
    application = models.ForeignKey(
        ShowcaseApplication,
        on_delete=models.CASCADE,
        related_name="attendance_records",
        null=True,
        blank=True,
        help_text="Optional — link to the participant's application row.",
    )
    participant = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.PROTECT,
        related_name="+",
    )
    attended = models.BooleanField(default=False)
    checked_in_at = models.DateTimeField(null=True, blank=True)
    checked_in_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    notes = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["event", "participant"]
        constraints = [
            models.UniqueConstraint(
                fields=["event", "participant"],
                name="uniq_smehub_event_attendance",
            ),
        ]
        indexes = [models.Index(fields=["event", "attended"])]

    def __str__(self) -> str:
        return f"{self.event.name}: {self.participant_id} {'✓' if self.attended else '✗'}"


class PitchScore(models.Model):
    """Judge score against a single rubric section (FRSME-ISD013).

    Polymorphic via the application FK — works for both event and challenge
    applications. Section reference is required only for challenges (events
    use a single composite score recorded with section=None).
    """

    application = models.ForeignKey(
        ShowcaseApplication,
        on_delete=models.CASCADE,
        related_name="pitch_scores",
    )
    judge = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.PROTECT,
        related_name="smehub_pitch_scores",
    )
    section = models.ForeignKey(
        ChallengeRubricSection,
        on_delete=models.PROTECT,
        related_name="pitch_scores",
        null=True,
        blank=True,
        help_text="Required for challenge applications; null for event composite scores.",
    )
    score = models.DecimalField(max_digits=5, decimal_places=2)
    comments = models.TextField(blank=True)
    submitted_at = models.DateTimeField(default=timezone.now)

    class Meta:
        ordering = ["-submitted_at"]
        constraints = [
            models.UniqueConstraint(
                fields=["application", "judge", "section"],
                name="uniq_smehub_pitch_score_section",
            ),
        ]

    def __str__(self) -> str:
        section = self.section.title if self.section else "overall"
        return f"{self.application.application_id}/{section}: {self.score}"


# ---------------------------------------------------------------------------
# Deal rooms  (FRSME-ISD014–016, ISD019)
# ---------------------------------------------------------------------------

class DealRoom(models.Model):
    """Secure space between an entrepreneur/business and a counterparty.

    FRSME-ISD014 — opens on connection acceptance / event match / etc.
    FRSME-ISD015 — counterparty is polymorphic (Investor / Partner / Buyer /
                   ServiceProvider) so a single table covers SP3 + SP4 + SP5.
    FRSME-ISD016 — auto-expiry via Celery beat after `expiry_date`.
    PRD A4       — multiple concurrent deal rooms per entrepreneur are allowed,
                   but a (business, counterparty) pair has at most one OPEN room.
    """

    class Status(models.TextChoices):
        OPEN = "open", "Open"
        EXPIRED = "expired", "Expired"
        AGREEMENT_REACHED = "agreement_reached", "Agreement reached"
        CLOSED = "closed", "Closed"

    room_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
    entrepreneur = models.ForeignKey(
        EntrepreneurProfile,
        on_delete=models.PROTECT,
        related_name="smehub_deal_rooms",
    )
    business = models.ForeignKey(
        Business,
        on_delete=models.PROTECT,
        related_name="smehub_deal_rooms",
    )
    counterparty_content_type = models.ForeignKey(
        ContentType,
        on_delete=models.PROTECT,
        related_name="+",
    )
    counterparty_object_id = models.CharField(max_length=64)
    counterparty = GenericForeignKey("counterparty_content_type", "counterparty_object_id")

    origin_content_type = models.ForeignKey(
        ContentType,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
        help_text="Optional — the ShowcaseEvent / Challenge / Connection that triggered this room.",
    )
    origin_object_id = models.CharField(max_length=64, blank=True)
    origin = GenericForeignKey("origin_content_type", "origin_object_id")

    title = models.CharField(max_length=255, blank=True)
    summary = models.TextField(blank=True)
    status = FSMField(
        max_length=24,
        choices=Status.choices,
        default=Status.OPEN,
        protected=True,
        db_index=True,
    )
    opened_at = models.DateTimeField(default=timezone.now)
    expiry_date = models.DateTimeField(
        help_text="Auto-expiry deadline (FRSME-ISD016 — Celery beat).",
    )
    closed_at = models.DateTimeField(null=True, blank=True)
    closed_reason = models.TextField(blank=True)
    last_activity_at = models.DateTimeField(default=timezone.now)
    # FRSME-ISD016 alt-A3 — idempotency marker for the expiry-warning beat.
    last_expiry_warning_at = models.DateTimeField(null=True, blank=True)
    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["-last_activity_at"]
        constraints = [
            # PRD A4 — at most one OPEN deal room per (business, counterparty) pair.
            # Multiple concurrent rooms across DIFFERENT counterparties remain allowed.
            models.UniqueConstraint(
                fields=[
                    "business",
                    "counterparty_content_type",
                    "counterparty_object_id",
                ],
                condition=models.Q(status="open"),
                name="uniq_smehub_open_deal_room_per_pair",
            ),
        ]
        indexes = [
            models.Index(fields=["entrepreneur", "status"]),
            models.Index(fields=["counterparty_content_type", "counterparty_object_id", "status"]),
            models.Index(fields=["status", "expiry_date"]),
        ]

    def __str__(self) -> str:
        return f"DealRoom {self.room_id} ({self.status})"

    @transition(field=status, source=Status.OPEN, target=Status.EXPIRED)
    def expire(self) -> None:
        self.closed_at = timezone.now()
        self.closed_reason = "Auto-expired (FRSME-ISD016)."

    @transition(field=status, source=Status.OPEN, target=Status.AGREEMENT_REACHED)
    def reach_agreement(self) -> None:
        self.closed_at = timezone.now()

    @transition(
        field=status,
        source=[Status.OPEN, Status.AGREEMENT_REACHED, Status.EXPIRED],
        target=Status.CLOSED,
    )
    def close(self, *, reason: str = "") -> None:
        self.closed_at = timezone.now()
        self.closed_reason = reason

    @transition(field=status, source=Status.EXPIRED, target=Status.OPEN)
    def reopen(self, *, new_expiry) -> None:
        self.closed_at = None
        self.closed_reason = ""
        self.expiry_date = new_expiry


class DealRoomMessage(models.Model):
    """Threaded message within a deal room. HTMX swap target."""

    deal_room = models.ForeignKey(
        DealRoom,
        on_delete=models.CASCADE,
        related_name="messages",
    )
    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()
    attachment = models.FileField(upload_to=upload_to_smehub_showcasing, null=True, 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=["deal_room", "created_at"])]

    def __str__(self) -> str:
        return f"Msg by {self.author_id} on {self.deal_room.room_id}"


class DealRoomDocument(models.Model):
    """Versioned document artefact (proposal / term sheet / due diligence / other)."""

    class Kind(models.TextChoices):
        PROPOSAL = "proposal", "Proposal"
        TERM_SHEET = "term_sheet", "Term sheet"
        DUE_DILIGENCE = "due_diligence", "Due diligence"
        OTHER = "other", "Other"

    deal_room = models.ForeignKey(
        DealRoom,
        on_delete=models.CASCADE,
        related_name="documents",
    )
    label = models.CharField(max_length=255)
    kind = models.CharField(max_length=24, choices=Kind.choices, default=Kind.OTHER, db_index=True)
    file = models.FileField(upload_to=upload_to_smehub_showcasing)
    version = models.PositiveSmallIntegerField(default=1)
    notes = models.TextField(blank=True)
    uploaded_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.PROTECT,
        related_name="+",
    )
    uploaded_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-uploaded_at"]
        constraints = [
            models.UniqueConstraint(
                fields=["deal_room", "label", "version"],
                name="uniq_smehub_deal_room_doc_version",
            ),
        ]

    def __str__(self) -> str:
        return f"{self.label} v{self.version}"


class DealAgreement(models.Model):
    """Formalised outcome of a deal room (FRSME-ISD019).

    Created by an admin once the parties have signed; flips the room to
    AGREEMENT_REACHED.
    """

    deal_room = models.OneToOneField(
        DealRoom,
        on_delete=models.CASCADE,
        related_name="agreement",
    )
    title = models.CharField(max_length=255)
    summary = models.TextField(blank=True)
    signed_file = models.FileField(upload_to=upload_to_smehub_showcasing)
    agreed_at = models.DateTimeField(default=timezone.now)
    formalised_by_admin = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.PROTECT,
        related_name="+",
    )
    notes = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self) -> str:
        return f"Agreement — {self.deal_room.room_id}"
