"""SME-Hub investment & funding access models — Subprocess SP5.

Implements PRD Tables 43–44 (FRSME-INV001 → FRSME-INV017):
  • Investor                       — directory of angels / VCs / DFIs / funders (FRSME-INV001–003)
  • FundingCall                    — public calls for proposals (FRSME-INV004–005)
  • FundingApplication             — entrepreneur application against a call (FRSME-INV006–007)
  • FundingApplicationDocument     — supporting documents (M2M-style inline)
  • InvestorMatch                  — AI-ranked investor recommendations (FRSME-INV008–009)
  • InvestmentReadiness            — readiness flag with thresholds (FRSME-INV010)
  • SMEPerformanceSnapshot         — JSON aggregating SP1–SP5 (FRSME-INV011–012)
  • InvestorConnectionRequest      — pitch request → DealRoom on accept (FRSME-INV013–015)
  • ManualFundingRecord            — admin-entered off-system funding (FRSME-INV016)
  • DashboardAccessLog             — read-only dashboard share log

FSMs live on Investor.verification_status, FundingCall.status,
FundingApplication.status, InvestorConnectionRequest.status. 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_investment
from apps.smehub.onboarding.models import Business, EntrepreneurProfile


# ---------------------------------------------------------------------------
# Investor directory  (FRSME-INV001–003)
# ---------------------------------------------------------------------------

class Investor(models.Model):
    """Directory of investors and funders that engage with the platform.

    FRSME-INV001 — admin maintains the directory; FRSME-INV002 — investors can
    self-register and land in PENDING; FRSME-INV003 — verification gates
    visibility to entrepreneurs and the matchmaking pipeline.
    """

    class Type(models.TextChoices):
        ANGEL = "angel", "Angel investor"
        VC = "vc", "Venture capital"
        DFI = "dfi", "Development finance institution"
        DONOR = "donor", "Donor / grant maker"
        FOUNDATION = "foundation", "Foundation"
        GOV_PROGRAMME = "gov_programme", "Government programme"

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

    org_name = models.CharField(max_length=255)
    type = models.CharField(max_length=24, choices=Type.choices, db_index=True)
    investment_focus = models.JSONField(
        default=list,
        blank=True,
        help_text="Sector tags (matches BUSINESS choices); FRSME-INV008 facet.",
    )
    investment_stages = models.JSONField(
        default=list,
        blank=True,
        help_text=(
            "Business-stage codes the investor backs (matches Business.business_stage). "
            "FRSME-INV008 audit fix — the matchmaker now uses this to score "
            "stage alignment instead of a hard-coded empty list."
        ),
    )
    geographic_preference = models.JSONField(
        default=list,
        blank=True,
        help_text="ISO-2 country codes the investor prefers.",
    )
    ticket_size_min = models.DecimalField(
        max_digits=14,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Minimum cheque size (USD).",
    )
    ticket_size_max = models.DecimalField(
        max_digits=14,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Maximum cheque size (USD).",
    )
    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)
    logo = models.FileField(upload_to=upload_to_smehub_investment, null=True, blank=True)
    contact_user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
        help_text="Account that maintains this investor profile (FRSME-INV002).",
    )
    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:
        ordering = ["org_name"]
        indexes = [
            models.Index(fields=["type", "verification_status"]),
        ]

    def __str__(self) -> str:
        return self.org_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:
        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 = ""


# ---------------------------------------------------------------------------
# Funding calls  (FRSME-INV004–005)
# ---------------------------------------------------------------------------

class FundingCall(models.Model):
    """Public funding call — seed grants, growth equity, challenge funds.

    FRSME-INV004 — admin publishes a call; FRSME-INV005 — eligible
    entrepreneurs are notified on publish.
    """

    class Type(models.TextChoices):
        SEED = "seed", "Seed grant"
        POC_GRANT = "poc_grant", "Proof-of-concept grant"
        GROWTH = "growth", "Growth investment"
        CHALLENGE_FUND = "challenge_fund", "Challenge fund"

    class Status(models.TextChoices):
        DRAFT = "draft", "Draft"
        PUBLISHED = "published", "Published"
        CLOSED = "closed", "Closed"

    title = models.CharField(max_length=255)
    summary = models.TextField(blank=True)
    description = models.TextField(blank=True)
    type = models.CharField(max_length=24, choices=Type.choices, db_index=True)
    eligibility = models.JSONField(
        default=dict,
        blank=True,
        help_text="Free-form eligibility payload.",
    )
    target_sectors = models.JSONField(default=list, blank=True)
    target_stages = models.JSONField(default=list, blank=True)
    target_countries = models.JSONField(default=list, blank=True)
    # FRSME-INV004 — structured eligibility criteria applied by the publish-time
    # match (in addition to the sector / stage / country / amount fields above),
    # replacing the old free-form ``eligibility`` JSON.
    require_incubation_complete = models.BooleanField(
        default=False,
        help_text="Only entrepreneurs who have completed an incubation programme are eligible.",
    )
    min_full_time_staff = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Minimum full-time staff (traction proxy). Blank = no minimum.",
    )
    amount_min = models.DecimalField(max_digits=14, decimal_places=2, null=True, blank=True)
    amount_max = models.DecimalField(max_digits=14, decimal_places=2, null=True, blank=True)
    application_deadline = models.DateTimeField()
    contact_email = models.EmailField(blank=True)
    cover_image = models.FileField(upload_to=upload_to_smehub_investment, null=True, blank=True)
    organiser = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="smehub_funding_calls_organised",
    )
    investor = models.ForeignKey(
        Investor,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="funding_calls",
        help_text="Optional — investor sponsoring the call.",
    )
    status = FSMField(
        max_length=16,
        choices=Status.choices,
        default=Status.DRAFT,
        protected=True,
        db_index=True,
    )
    published_at = models.DateTimeField(null=True, blank=True)
    closed_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 = ["-application_deadline"]
        indexes = [
            models.Index(fields=["status", "application_deadline"]),
            models.Index(fields=["type", "status"]),
        ]

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

    @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.CLOSED)
    def close(self) -> None:
        self.closed_at = timezone.now()

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


# ---------------------------------------------------------------------------
# Funding applications  (FRSME-INV006–007)
# ---------------------------------------------------------------------------

class FundingApplication(models.Model):
    """Entrepreneur application against a FundingCall.

    Mirrors SP2 incubation Application FSM. HTMX auto-save uses
    auto_saved_payload during draft. FRSME-INV006–007.
    """

    class Status(models.TextChoices):
        DRAFT = "draft", "Draft (auto-saved)"
        SUBMITTED = "submitted", "Submitted"
        UNDER_REVIEW = "under_review", "Under review"
        ACCEPTED = "accepted", "Accepted"
        REJECTED = "rejected", "Rejected"
        RETURNED_FOR_INFO = "returned_for_info", "Returned for more info"
        WITHDRAWN = "withdrawn", "Withdrawn"

    funding_call = models.ForeignKey(
        FundingCall,
        on_delete=models.CASCADE,
        related_name="applications",
    )
    entrepreneur = models.ForeignKey(
        EntrepreneurProfile,
        on_delete=models.PROTECT,
        related_name="smehub_funding_applications",
    )
    business = models.ForeignKey(
        Business,
        on_delete=models.PROTECT,
        related_name="smehub_funding_applications",
    )
    application_id = models.CharField(
        max_length=20,
        unique=True,
        editable=False,
    )
    status = FSMField(
        max_length=24,
        choices=Status.choices,
        default=Status.DRAFT,
        protected=True,
        db_index=True,
    )
    funding_request_amount = models.DecimalField(
        max_digits=14,
        decimal_places=2,
        null=True,
        blank=True,
    )
    business_plan_file = models.FileField(
        upload_to=upload_to_smehub_investment,
        null=True,
        blank=True,
    )
    financial_projections_file = models.FileField(
        upload_to=upload_to_smehub_investment,
        null=True,
        blank=True,
    )
    pitch_summary = models.TextField(blank=True)
    use_of_funds = models.TextField(blank=True)
    traction = models.TextField(blank=True)
    team_summary = models.TextField(blank=True)
    auto_saved_payload = models.JSONField(default=dict, blank=True)
    submitted_at = models.DateTimeField(null=True, blank=True)
    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)
    return_message = models.TextField(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=["funding_call", "business"],
                condition=models.Q(
                    status__in=[
                        "draft",
                        "submitted",
                        "under_review",
                        "returned_for_info",
                    ],
                ),
                name="uniq_smehub_active_funding_application",
            ),
        ]
        indexes = [
            models.Index(fields=["funding_call", "status"]),
            models.Index(fields=["entrepreneur", "status"]),
        ]

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

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

    @transition(field=status, source=Status.DRAFT, target=Status.SUBMITTED)
    def submit(self) -> None:
        self.submitted_at = timezone.now()
        self.auto_saved_payload = {}

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

    @transition(
        field=status,
        source=[Status.SUBMITTED, Status.UNDER_REVIEW],
        target=Status.ACCEPTED,
    )
    def accept(self, *, by_user, note: str = "") -> None:
        self.decision_by = by_user
        self.decided_at = timezone.now()
        self.decision_note = note

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

    @transition(
        field=status,
        source=[Status.SUBMITTED, Status.UNDER_REVIEW],
        target=Status.RETURNED_FOR_INFO,
    )
    def return_for_info(self, *, message: str) -> None:
        self.return_message = message

    @transition(field=status, source=Status.RETURNED_FOR_INFO, target=Status.SUBMITTED)
    def resubmit(self) -> None:
        self.return_message = ""
        self.submitted_at = timezone.now()

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


class FundingApplicationDocument(models.Model):
    """Supporting document attached to a funding application."""

    application = models.ForeignKey(
        FundingApplication,
        on_delete=models.CASCADE,
        related_name="supporting_documents",
    )
    label = models.CharField(max_length=255)
    file = models.FileField(upload_to=upload_to_smehub_investment)
    uploaded_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-uploaded_at"]

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


# ---------------------------------------------------------------------------
# Investor matches  (FRSME-INV008–009)
# ---------------------------------------------------------------------------

class InvestorMatch(models.Model):
    """AI-generated investor recommendation per (entrepreneur, investor)."""

    entrepreneur = models.ForeignKey(
        EntrepreneurProfile,
        on_delete=models.CASCADE,
        related_name="investor_matches",
    )
    investor = models.ForeignKey(
        Investor,
        on_delete=models.CASCADE,
        related_name="entrepreneur_matches",
    )
    score = models.DecimalField(max_digits=6, decimal_places=4, default=Decimal("0"))
    reason = models.TextField(blank=True)
    is_fallback = models.BooleanField(default=True)
    generated_at = models.DateTimeField(default=timezone.now)
    refreshed_by_signal = models.CharField(max_length=64, blank=True)

    class Meta:
        ordering = ["-score", "-generated_at"]
        constraints = [
            models.UniqueConstraint(
                fields=["entrepreneur", "investor"],
                name="uniq_smehub_investor_match",
            ),
        ]
        indexes = [
            models.Index(fields=["entrepreneur", "score"]),
        ]

    def __str__(self) -> str:
        return f"{self.entrepreneur.user.email} ↔ {self.investor.org_name} ({self.score})"


# ---------------------------------------------------------------------------
# Investment readiness  (FRSME-INV010)
# ---------------------------------------------------------------------------

class InvestmentReadiness(models.Model):
    """Readiness flag per entrepreneur with breakdown of threshold checks."""

    entrepreneur = models.OneToOneField(
        EntrepreneurProfile,
        on_delete=models.CASCADE,
        related_name="investment_readiness",
    )
    is_ready = models.BooleanField(default=False)
    threshold_breakdown = models.JSONField(
        default=dict,
        blank=True,
        help_text="Per-criterion booleans (verified_business, baseline_set, "
        "incubation_complete, has_partnerships, etc.).",
    )
    recommended_actions = models.JSONField(
        default=list,
        blank=True,
        help_text="List of {action, link, reason} dicts shown on the readiness card.",
    )
    last_evaluated_at = models.DateTimeField(default=timezone.now)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name_plural = "Investment readiness records"

    def __str__(self) -> str:
        return f"Readiness({self.entrepreneur.user.email}): {self.is_ready}"


class ReadinessThresholdConfig(models.Model):
    """FRSME-INV010 — admin-configurable investment-readiness thresholds.

    ``compute_readiness`` reads the most recently updated active row so admins
    can tune the bar (min partnerships, incubation requirement, etc.) without a
    code change. When no active row exists the hard-coded defaults apply.
    """

    name = models.CharField(max_length=120, default="Default thresholds")
    verified_business_required = models.BooleanField(default=True)
    baseline_required = models.BooleanField(default=True)
    incubation_completion_required = models.BooleanField(default=False)
    min_partnerships = models.PositiveIntegerField(default=1)
    min_showcase_or_catalogue = models.PositiveIntegerField(default=0)
    min_funding_events = models.PositiveIntegerField(default=0)
    is_active = models.BooleanField(default=True, db_index=True)
    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 = ["-updated_at"]

    def __str__(self) -> str:
        return f"{self.name} ({'active' if self.is_active else 'inactive'})"

    def as_thresholds(self) -> dict:
        return {
            "verified_business_required": self.verified_business_required,
            "baseline_required": self.baseline_required,
            "incubation_completion_required": self.incubation_completion_required,
            "min_partnerships": self.min_partnerships,
            "min_showcase_or_catalogue": self.min_showcase_or_catalogue,
            "min_funding_events": self.min_funding_events,
        }


# ---------------------------------------------------------------------------
# SME performance snapshot  (FRSME-INV011–012)
# ---------------------------------------------------------------------------

class SMEPerformanceSnapshot(models.Model):
    """Aggregated cross-SP snapshot. Cached per entrepreneur."""

    entrepreneur = models.OneToOneField(
        EntrepreneurProfile,
        on_delete=models.CASCADE,
        related_name="performance_snapshot",
    )
    business = models.ForeignKey(
        Business,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="performance_snapshots",
        help_text="Optional — primary business covered by the snapshot.",
    )
    payload = models.JSONField(
        default=dict,
        blank=True,
        help_text=(
            "Full aggregation: sp1 baseline, sp2 programmes, sp3 partnerships, "
            "sp4 commercialisation, sp5 funding events. FRSME-INV011–012."
        ),
    )
    version = models.PositiveSmallIntegerField(default=1)
    refreshed_at = models.DateTimeField(default=timezone.now)
    refreshed_by_signal = models.CharField(max_length=64, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name_plural = "SME performance snapshots"

    def __str__(self) -> str:
        return f"Snapshot v{self.version} — {self.entrepreneur.user.email}"


# ---------------------------------------------------------------------------
# Investor connection requests  (FRSME-INV013–015)
# ---------------------------------------------------------------------------

class InvestorConnectionRequest(models.Model):
    """Entrepreneur-initiated pitch request to an investor.

    On accept, opens a DealRoom via apps.smehub.showcasing.services.open_deal_room
    so SP4 deal-room infra is the single source of truth (FRSME-INV015).
    """

    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="investor_connection_requests",
    )
    business = models.ForeignKey(
        Business,
        on_delete=models.PROTECT,
        related_name="investor_connection_requests",
    )
    investor = models.ForeignKey(
        Investor,
        on_delete=models.CASCADE,
        related_name="connection_requests",
    )
    message = models.TextField(blank=True)
    funding_ask = models.DecimalField(
        max_digits=14,
        decimal_places=2,
        null=True,
        blank=True,
        help_text="Indicative funding amount sought (USD); optional but recommended.",
    )
    supporting_link = models.URLField(
        blank=True,
        help_text="Pitch deck, demo video, or one-pager URL.",
    )
    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="+",
    )
    deal_room = models.ForeignKey(
        "smehub_showcasing.DealRoom",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
        help_text="Set when accepted — FRSME-INV015.",
    )
    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", "investor", "business"],
                condition=models.Q(status__in=["pending", "accepted"]),
                name="uniq_smehub_active_investor_request",
            ),
        ]
        indexes = [
            models.Index(fields=["investor", "status"]),
            models.Index(fields=["entrepreneur", "status"]),
        ]

    def __str__(self) -> str:
        return f"{self.entrepreneur.user.email} → {self.investor.org_name} ({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


# ---------------------------------------------------------------------------
# Manual funding records  (FRSME-INV016)
# ---------------------------------------------------------------------------

class ManualFundingRecord(models.Model):
    """Admin-entered off-system funding event.

    FRSME-INV016 — investor secured outside the platform but should still
    flow into the M&EL longitudinal record.
    """

    entrepreneur = models.ForeignKey(
        EntrepreneurProfile,
        on_delete=models.PROTECT,
        related_name="manual_funding_records",
    )
    business = models.ForeignKey(
        Business,
        on_delete=models.PROTECT,
        related_name="manual_funding_records",
    )
    funder_name = models.CharField(max_length=255)
    investor = models.ForeignKey(
        Investor,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="manual_funding_records",
        help_text="Optional link to a directory investor.",
    )
    amount = models.DecimalField(max_digits=14, decimal_places=2)
    currency = models.CharField(max_length=8, default="USD")
    funded_at = models.DateField()
    source_note = models.TextField(blank=True)
    captured_by_admin = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.PROTECT,
        related_name="+",
    )
    created_at = models.DateTimeField(auto_now_add=True)

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

    def __str__(self) -> str:
        return f"{self.funder_name}: {self.currency} {self.amount} → {self.business.name}"


# ---------------------------------------------------------------------------
# Dashboard access log  (FRSME-INV012 — read-only sharing)
# ---------------------------------------------------------------------------

class DashboardAccessLog(models.Model):
    """Audit row for every read-only performance dashboard share / open."""

    class Scope(models.TextChoices):
        SUMMARY = "summary", "Summary"
        FULL = "full", "Full"

    entrepreneur = models.ForeignKey(
        EntrepreneurProfile,
        on_delete=models.CASCADE,
        related_name="dashboard_access_logs",
    )
    investor = models.ForeignKey(
        Investor,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="dashboard_access_logs",
    )
    viewer_content_type = models.ForeignKey(
        ContentType,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
    )
    viewer_object_id = models.CharField(max_length=64, blank=True)
    viewer = GenericForeignKey("viewer_content_type", "viewer_object_id")
    scope = models.CharField(
        max_length=16,
        choices=Scope.choices,
        default=Scope.SUMMARY,
    )
    share_token = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
    accessed_at = models.DateTimeField(default=timezone.now)
    note = models.CharField(max_length=255, blank=True)
    # FRSME-INV012 audit fix — bounded share lifetime, explicit revocation,
    # and a per-load view counter so a leaked share URL has finite blast radius.
    expires_at = models.DateTimeField(null=True, blank=True, db_index=True)
    revoked_at = models.DateTimeField(null=True, blank=True)
    last_viewed_at = models.DateTimeField(null=True, blank=True)
    view_count = models.PositiveIntegerField(default=0)

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

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

    @property
    def is_active(self) -> bool:
        """A share is active iff it has not been revoked AND has not expired."""
        if self.revoked_at is not None:
            return False
        if self.expires_at is not None and self.expires_at < timezone.now():
            return False
        return True


class DashboardViewEvent(models.Model):
    """One row per actual *render* of the shared dashboard. Separate from
    ``DashboardAccessLog`` (which captures the grant) so the access trail
    distinguishes "was shared" from "was actually opened" — required by
    FRSME-INV012's audit log contract.
    """

    access_log = models.ForeignKey(
        DashboardAccessLog,
        on_delete=models.CASCADE,
        related_name="view_events",
    )
    viewer_user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    viewed_at = models.DateTimeField(default=timezone.now, db_index=True)
    ip_address = models.GenericIPAddressField(null=True, blank=True)
    user_agent = models.CharField(max_length=512, blank=True)

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

    def __str__(self) -> str:  # pragma: no cover - admin display only
        return f"View({self.access_log_id}) @ {self.viewed_at:%Y-%m-%d %H:%M}"
