import uuid

from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.urls import reverse
from django.utils import timezone


NOTE_UNLOCK_POST_TITLE_HINT = "Notes & recognitions"  # see drama.NOTE_UNLOCK_POST_TITLE; copy lives there
TAX_LEDGER_POST_TITLE = "Debits & credits"
MAIL_ACCEPTANCE_POST_TITLE = "Acceptances & rejections"


class Post(models.Model):
    KIND_NOTE_UNLOCK   = "note_unlock"
    KIND_USER_POST     = "user_post"
    KIND_SHARE_INVITE  = "share_invite"
    # Per-user @taxman-authored ledger (user-spec 2026-05-26). Each FREE/PAID
    # DRAW spend at /gameboard/my-sea/ appends one Line via
    # `apps.billboard.tax.log_tax_debit`. Mirrors the NOTE_UNLOCK Post pattern:
    # one Post per user, system-authored, readonly textarea in post.html.
    KIND_TAX_LEDGER    = "tax_ledger"
    # Per-user @mailman-authored "Acceptances & rejections" log (my-sea bud-
    # invite flow, see [[my-sea-invite-voice-blueprint]]). Each invite appends
    # one interactive Line (OK/BYE buttons) via `apps.billboard.mail.
    # log_sea_invite`. Mirrors the TAX_LEDGER Post pattern: one Post per
    # invitee, system-authored, with admin_solicited Lines.
    KIND_MAIL_ACCEPTANCE = "mail_acceptance"
    KIND_CHOICES = [
        (KIND_NOTE_UNLOCK,     "Note unlocks"),
        (KIND_USER_POST,       "User post"),
        (KIND_SHARE_INVITE,    "Share invites"),
        (KIND_TAX_LEDGER,      "Debits & credits"),
        (KIND_MAIL_ACCEPTANCE, "Acceptances & rejections"),
    ]

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    owner = models.ForeignKey(
        "lyric.User",
        related_name="posts",
        blank=True,
        null=True,
        on_delete=models.CASCADE,
    )

    shared_with = models.ManyToManyField(
        "lyric.User",
        related_name="shared_posts",
        blank=True,
    )

    # `kind` discriminates per-category Posts — e.g. Note.grant_if_new appends
    # to the user's single (owner=user, kind=NOTE_UNLOCK) Post; user-authored
    # composes default to KIND_USER_POST.
    kind = models.CharField(
        max_length=32,
        choices=KIND_CHOICES,
        default=KIND_USER_POST,
    )

    # Stored title — set explicitly on creation. Note-unlock Posts hardcode
    # "Notes & recognitions"; user_post Posts truncate first line to 35 chars
    # (32 + "..." past length). Replaces the legacy `name` property which
    # gleaned `lines.first().text` lazily and broke if the first Line was
    # later edited or deleted.
    title = models.CharField(max_length=35, default="")

    def get_absolute_url(self):
        return reverse("billboard:view_post", args=[self.id])


class Line(models.Model):
    text = models.TextField(default="")
    post = models.ForeignKey(Post, default=None, on_delete=models.CASCADE, related_name="lines")
    # `author` PROTECTs against accidental sitewide-entity deletion (notably
    # `adman`, the system-author for note_unlock + share_invite Lines).
    # User-typed Lines attribute to the typing User; system-rendered Lines
    # attribute to adman so the per-line "username" column always renders.
    author = models.ForeignKey(
        "lyric.User",
        on_delete=models.PROTECT,
        related_name="authored_lines",
    )
    created_at = models.DateTimeField(auto_now_add=True)
    # System-authored Lines on NOTE_UNLOCK Posts must set this True; the
    # post_save signal below deletes any Line on a NOTE_UNLOCK Post w.o.
    # this flag (defense-in-depth alongside view_post's POST guard).
    admin_solicited = models.BooleanField(default=False)

    class Meta:
        ordering = ("created_at", "id")
        unique_together = ("post", "text")

    def __str__(self):
        return self.text

    @property
    def display_text(self):
        """User-facing line text. For TAX_LEDGER lines, strips the leading
        `[<ISO timestamp>] ` prefix that the `apps.billboard.tax.log_tax_
        debit` helper bakes in to satisfy `unique_together = (post, text)`
        on repeat-slug spends — the Brief carries `created_at` and the
        Post line carries `created_at` independently, so embedding a third
        timestamp in the prose is noise.

        For non-tax lines this is identity (returns `text` unchanged) —
        Note-unlock + user-typed Lines have no prefix to strip."""
        if self.post.kind == Post.KIND_TAX_LEDGER and self.text.startswith("["):
            close = self.text.find("] ")
            if close != -1:
                return self.text[close + 2:]
        return self.text


class Brief(models.Model):
    """A slide-down notification record. Owner = whose attention; post = where
    FYI navigates (and where mark-read happens on GET); line = the specific
    appended Line that triggered it (so the banner can surface its text).

    `kind` discriminates the affordances the banner renders. NOTE_UNLOCK
    Briefs get a clickable square that jumps direct to my_notes.html;
    SHARE_INVITE Briefs render the invitation copy; USER_POST is the legacy
    user-authored compose flow.

    Magic-link confirmation + invalid-link banners use the same Gaussian-glass
    visual styling but ride no Brief row (transient one-shot).
    """
    KIND_NOTE_UNLOCK   = "note_unlock"
    KIND_USER_POST     = "user_post"
    KIND_SHARE_INVITE  = "share_invite"
    KIND_GAME_INVITE   = "game_invite"
    # Tax-ledger Briefs (FREE/PAID DRAW spend, user-spec 2026-05-26). FYI
    # navigates to the user's TAX_LEDGER Post. NVM POSTs to the dismiss-
    # brief endpoint so the dismissal persists per-cycle (see
    # `dismiss_url` in `to_banner_dict`).
    KIND_TAX_LEDGER    = "tax_ledger"
    # Surfaces the invitee's slide-down notification when an owner invites them
    # to a my-sea table; FYI navigates to their "Acceptances & rejections" Post.
    KIND_MAIL_ACCEPTANCE = "mail_acceptance"
    KIND_CHOICES = [
        (KIND_NOTE_UNLOCK,     "Note unlock"),
        (KIND_USER_POST,       "User post"),
        (KIND_SHARE_INVITE,    "Share invite"),
        (KIND_GAME_INVITE,     "Game invite"),
        (KIND_TAX_LEDGER,      "Tax ledger"),
        (KIND_MAIL_ACCEPTANCE, "Mail acceptance"),
    ]

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    owner = models.ForeignKey(
        "lyric.User",
        related_name="briefs",
        on_delete=models.CASCADE,
    )
    # Post is nullable now: KIND_GAME_INVITE briefs ride on a Room FK
    # instead of a Post (the gatekeeper invite confirmation has no post
    # to navigate to). Post FKs only set for note_unlock / user_post /
    # share_invite kinds.
    post = models.ForeignKey(
        Post,
        related_name="briefs",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
    )
    # Room FK — set only on KIND_GAME_INVITE briefs; FYI navigates to
    # the gatekeeper page for that room.
    room = models.ForeignKey(
        "epic.Room",
        related_name="briefs",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
    )
    # Line is nullable because a share_invite-style Brief can race ahead of its
    # async-appended Line write; the post FK alone is enough to navigate.
    line = models.ForeignKey(
        Line,
        related_name="briefs",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
    )
    is_unread = models.BooleanField(default=True)
    kind = models.CharField(
        max_length=32,
        choices=KIND_CHOICES,
        default=KIND_USER_POST,
    )
    title = models.CharField(max_length=255, blank=True)
    created_at = models.DateTimeField(default=timezone.now)

    class Meta:
        ordering = ["-created_at"]

    def __str__(self):
        return (
            f"Brief({self.kind}, {self.owner.email}, "
            f"unread={self.is_unread})"
        )

    def to_banner_dict(self):
        """Shape this Brief for the slide-down banner JS. NOTE_UNLOCK kind
        carries a square_url pointing at /billboard/my-notes/ so the
        thumbnail-square inside the banner jumps direct to the user's Note
        collection. GAME_INVITE kind has no Post — the FYI link navigates
        to the gatekeeper page for the brief's Room instead.

        `dismiss_url` (TAX_LEDGER only — user-spec 2026-05-26): the POST
        endpoint the banner's NVM btn fires to so the dismissal persists
        per-cycle (FREE DRAW until next FREE DRAW spend, PAID DRAW until
        next PAID DRAW commit). Empty for kinds with no persistence."""
        square_url = ""
        dismiss_url = ""
        if self.kind == self.KIND_NOTE_UNLOCK:
            square_url = reverse("billboard:my_notes")
        if self.post_id:
            post_url = self.post.get_absolute_url()
        elif self.room_id:
            post_url = reverse("epic:gatekeeper", args=[self.room_id])
        else:
            post_url = ""
        return {
            "id":          str(self.id),
            "kind":        self.kind,
            "title":       self.title,
            # `display_text` strips the `[<iso timestamp>] ` prefix on
            # TAX_LEDGER lines (the prefix exists only to satisfy Line's
            # `unique_together = (post, text)` invariant on repeat-slug
            # spends — the Brief's own `created_at` slot below covers the
            # user-facing timestamp). Identity for all other line kinds.
            "line_text":   self.line.display_text if self.line else "",
            "post_url":    post_url,
            "square_url":  square_url,
            "dismiss_url": dismiss_url,
            "created_at":  self.created_at.isoformat(),
        }


# ── Listener: nuke unsolicited Lines on system-author Posts ──────────────
# Defense-in-depth alongside view_post's POST guard. A Line saved on a
# NOTE_UNLOCK / TAX_LEDGER / MAIL_ACCEPTANCE Post that lacks
# admin_solicited=True (e.g. a stray ORM-level write or an API path that
# bypasses the view) gets deleted right after the save. `Note.grant_if_new`,
# `apps.billboard.tax.log_tax_debit`, and `apps.billboard.mail.log_sea_invite`
# all set admin_solicited=True on their Lines so legitimate system prose
# survives.

_SYSTEM_AUTHOR_POST_KINDS = (
    Post.KIND_NOTE_UNLOCK,
    Post.KIND_TAX_LEDGER,
    Post.KIND_MAIL_ACCEPTANCE,
)


@receiver(post_save, sender=Line)
def _delete_unsolicited_admin_post_lines(sender, instance, created, **kwargs):
    if not created:
        return
    if instance.post.kind in _SYSTEM_AUTHOR_POST_KINDS and not instance.admin_solicited:
        instance.delete()


class BudshipNote(models.Model):
    """Per-relation personal note about a bud — bud landing page sprint
    2026-05-27 ([[project-bud-landing-page-sprint]]). One row per
    (user, bud) pair: the user's own shoptalk about that bud, NEVER
    visible to the bud. Lazy-created on first shoptalk save so the
    absence of a row reads as 'never edited' (drives the `.tt-milestone`
    slot on the My Buds tooltip portal — present when ≥1 edit, absent
    otherwise)."""

    user = models.ForeignKey(
        "lyric.User",
        on_delete=models.CASCADE,
        related_name="budship_notes_written",
    )
    bud = models.ForeignKey(
        "lyric.User",
        on_delete=models.CASCADE,
        related_name="budship_notes_about",
    )
    shoptalk = models.CharField(max_length=160, default="")
    edited_at = models.DateTimeField(auto_now=True)

    class Meta:
        unique_together = ("user", "bud")
        ordering = ("-edited_at",)

    def __str__(self):
        return f"BudshipNote({self.user_id} → {self.bud_id})"
