from django.conf import settings
from django.db import models

from apps.lyric.models import resolve_pronouns


def _actor_pronouns(actor):
    """Return (subj, obj, poss) for an event actor; default = pluralism when None."""
    return resolve_pronouns(getattr(actor, "pronouns", None) if actor else None)


class GameEvent(models.Model):
    # Gate phase
    ROOM_CREATED = "room_created"
    SLOT_RESERVED = "slot_reserved"
    SLOT_FILLED = "slot_filled"
    SLOT_RETURNED = "slot_returned"
    SLOT_RELEASED = "slot_released"
    INVITE_SENT = "invite_sent"
    # Role Select phase
    ROLE_SELECT_STARTED = "role_select_started"
    ROLE_SELECTED = "role_selected"
    ROLES_REVEALED = "roles_revealed"
    # Sig Select phase
    SIG_READY = "sig_ready"
    SIG_UNREADY = "sig_unready"
    # Sky Select phase
    SKY_SAVED = "sky_saved"

    VERB_CHOICES = [
        (ROOM_CREATED, "Room created"),
        (SLOT_RESERVED, "Gate slot reserved"),
        (SLOT_FILLED, "Gate slot filled"),
        (SLOT_RETURNED, "Gate slot returned"),
        (SLOT_RELEASED, "Gate slot released"),
        (INVITE_SENT, "Invite sent"),
        (ROLE_SELECT_STARTED, "Role select started"),
        (ROLE_SELECTED, "Role selected"),
        (ROLES_REVEALED, "Roles revealed"),
        (SIG_READY, "Sig claim staked"),
        (SIG_UNREADY, "Sig claim withdrawn"),
        (SKY_SAVED, "Sky saved"),
    ]

    room = models.ForeignKey(
        "epic.Room", on_delete=models.CASCADE, related_name="events",
    )
    actor = models.ForeignKey(
        settings.AUTH_USER_MODEL, null=True, blank=True,
        on_delete=models.SET_NULL, related_name="game_events",
    )
    verb = models.CharField(max_length=30, choices=VERB_CHOICES)
    data = models.JSONField(default=dict)
    timestamp = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["timestamp"]

    def to_prose(self):
        """Return a human-readable action description (actor rendered separately in template)."""
        d = self.data
        if self.verb == self.SLOT_FILLED:
            _token_names = {
                "coin": "Coin-on-a-String", "Free": "Free Token",
                "tithe": "Tithe Token", "pass": "Backstage Pass", "carte": "Carte Blanche",
            }
            code = d.get("token_type", "token")
            token = d.get("token_display") or _token_names.get(code, code)
            days = d.get("renewal_days", 7)
            slot = d.get("slot_number", "?")
            return f"deposits a {token} for slot {slot} (expires in {days} days)."
        if self.verb == self.SLOT_RESERVED:
            return "reserves a seat"
        if self.verb == self.SLOT_RETURNED:
            return "withdraws from the gate"
        if self.verb == self.SLOT_RELEASED:
            return f"releases slot {d.get('slot_number', '?')}"
        if self.verb == self.ROOM_CREATED:
            # First scroll log on a fresh room — system-authored greeting
            # (actor=None upstream). Format intentionally drops the actor
            # prefix the rest of the verbs use; the room's title is what
            # the welcome line celebrates.
            return f"Welcome to {self.room.name}!"
        if self.verb == self.INVITE_SENT:
            return "sends an invitation"
        if self.verb == self.ROLE_SELECT_STARTED:
            return "Role selection begins"
        if self.verb == self.ROLE_SELECTED:
            _role_names = {
                "PC": "Player", "BC": "Builder", "SC": "Shepherd",
                "AC": "Alchemist", "NC": "Narrator", "EC": "Economist",
            }
            _chair_order = ["PC", "NC", "EC", "SC", "AC", "BC"]
            _ordinals = ["1st", "2nd", "3rd", "4th", "5th", "6th"]
            code = d.get("role", "?")
            role = d.get("role_display") or _role_names.get(code, code)
            try:
                ordinal = _ordinals[_chair_order.index(code)]
            except ValueError:
                ordinal = "?"
            subj, _, _ = _actor_pronouns(self.actor)
            return f"assumes {ordinal} Chair; {subj} will start the game as the {role}."
        if self.verb == self.ROLES_REVEALED:
            return "All roles assigned"
        if self.verb == self.SIG_READY:
            card_name = d.get("card_name", "a card")
            corner_rank = d.get("corner_rank", "")
            suit_icon = d.get("suit_icon", "")
            if corner_rank:
                icon_html = f' <i class="fa-solid {suit_icon}"></i>' if suit_icon else ""
                abbrev = f" ({corner_rank}{icon_html})"
            else:
                abbrev = ""
            # Trump cards ("The Schizo", "The Nomad", "The Wanderer") drop
            # their "The " in this rendering: the prose template already
            # supplies "the", and a levity/gravity qualifier (e.g. "Engraven"
            # in "Engraven The Nomad") needs to butt up against the proper name.
            card_name = card_name.replace("The ", "", 1)
            _, _, poss = _actor_pronouns(self.actor)
            return f"embodies as {poss} Significator the {card_name}{abbrev}."
        if self.verb == self.SIG_UNREADY:
            _, _, poss = _actor_pronouns(self.actor)
            return f"disembodies {poss} Significator."
        if self.verb == self.SKY_SAVED:
            _, obj, poss = _actor_pronouns(self.actor)
            caps = list(d.get("top_capacitors") or [])
            if not caps:
                return f"beholds the skyscape of {poss} birth."
            if len(caps) == 1:
                return (
                    f"beholds the skyscape of {poss} birth, "
                    f"which yields {obj} a unique {caps[0]} capacity."
                )
            # Tied highest: "equal X and Y capacities" (2-way) or
            # "equal X, Y, and Z capacities" (3+, Oxford comma).
            if len(caps) == 2:
                joined = f"{caps[0]} and {caps[1]}"
            else:
                joined = ", ".join(caps[:-1]) + f", and {caps[-1]}"
            return (
                f"beholds the skyscape of {poss} birth, "
                f"which yields {obj} equal {joined} capacities."
            )
        return self.verb

    @property
    def struck(self):
        """True when this SIG_READY event was subsequently retracted (WAIT NVM)."""
        return self.data.get("retracted", False)

    def to_activity(self, base_url):
        """Serialise this event as an AS2 Activity dict, or None if unsupported."""
        if not self.actor or not self.actor.username:
            return None
        actor_url = f"{base_url}/ap/users/{self.actor.username}/"
        room_url = f"{base_url}/gameboard/room/{self.room_id}/"
        if self.verb == self.SLOT_FILLED:
            return {
                "type": "earthman:JoinGate",
                "actor": actor_url,
                "object": room_url,
                "summary": self.to_prose(),
            }
        if self.verb == self.ROLE_SELECTED:
            return {
                "type": "earthman:SelectRole",
                "actor": actor_url,
                "object": room_url,
                "summary": self.to_prose(),
            }
        if self.verb == self.ROOM_CREATED:
            return {
                "type": "Create",
                "actor": actor_url,
                "object": room_url,
                "summary": self.to_prose(),
            }
        return None

    def __str__(self):
        actor = self.actor.email if self.actor else "system"
        return f"[{self.timestamp:%Y-%m-%d %H:%M}] {actor} → {self.verb}"


class ScrollPosition(models.Model):
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
        related_name="scroll_positions",
    )
    room = models.ForeignKey(
        "epic.Room", on_delete=models.CASCADE,
        related_name="scroll_positions",
    )
    position = models.PositiveIntegerField(default=0)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        unique_together = [("user", "room")]

    def __str__(self):
        return f"{self.user.email} @ {self.room.name}: {self.position}px"


def record(room, verb, actor=None, **data):
    """Record a game event in the drama log."""
    return GameEvent.objects.create(room=room, actor=actor, verb=verb, data=data)


_NOTE_DISPLAY = {
    "stargazer":    {"greeting": "Welcome,",                                   "title": "Stargazer"},
    "schizo":       {"greeting": "Welcome,",                                   "title": "Schizo"},
    "nomad":        {"greeting": "Welcome,",                                   "title": "Nomad"},
    "super-schizo": {"greeting": "21<span class='ord'>st</span> Century",      "title": "Schizoid Man"},
    "super-nomad":  {"greeting": "Howdy,",                                     "title": "Stranger"},
    "baltimorean":  {"greeting": "Ayo,",                                       "title": "Ard!", "card_title": "Baltimorean"},
}

# Note slugs whose grant prose uses the long admin format ("The administration
# recognizes…") rather than the standard "Look!—new Note unlocked…" format.
# Any slug not in this set gets the standard format.
_ADMIN_NOTE_SLUGS = frozenset({"super-schizo", "super-nomad"})

# Hardcoded title for the per-user "Note unlocks" Post — supplants any
# first-line-glean for posts of kind=NOTE_UNLOCK.
NOTE_UNLOCK_POST_TITLE = "Notes & recognitions"


class Note(models.Model):
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
        related_name="notes",
    )
    slug = models.SlugField(max_length=60)
    earned_at = models.DateTimeField()
    palette = models.CharField(max_length=60, null=True, blank=True)

    class Meta:
        unique_together = [("user", "slug")]
        ordering = ["earned_at"]

    def __str__(self):
        return f"{self.user.email} — {self.slug}"

    @property
    def display_title(self):
        return _NOTE_DISPLAY.get(self.slug, {}).get("title", self.slug.replace("-", " ").title())

    @property
    def display_greeting(self):
        return _NOTE_DISPLAY.get(self.slug, {}).get("greeting", "Welcome,")

    @property
    def card_title(self):
        """The string shown in the my-notes card's "Title:" row. Defaults to
        `display_title` (the don-able title — most slugs render the title you
        DON here, e.g. "Schizoid Man" for super-schizo). Baltimorean overrides
        so the card reads "Baltimorean" instead of the navbar-only "Ard!"
        flair."""
        return _NOTE_DISPLAY.get(self.slug, {}).get("card_title", self.display_title)

    @property
    def display_name(self):
        """The Note's *name* (e.g., "Stargazer", "Super-Schizo") — the heading
        rendered on the my-notes card. Distinct from `display_title` which is
        the *recognition title* the user dons (e.g., "Schizoid Man" for
        super-schizo). For all current slugs `slug.title()` recovers the right
        casing (.title() capitalizes after non-letter chars, so "super-schizo"
        → "Super-Schizo"); special-case in `_NOTE_DISPLAY[slug]["name"]` if a
        future slug needs a different rendering."""
        return _NOTE_DISPLAY.get(self.slug, {}).get("name", self.slug.title())

    @classmethod
    def grant_if_new(cls, user, slug):
        """Grants the Note if it doesn't already exist on the user; on a fresh
        grant ALSO appends a Line to the user's per-category "Notes &
        recognitions" Post (creating the Post on first-ever unlock) and spawns
        a Brief that FKs the appended Line. Returns ``(note, created, brief)``
        — brief is None on idempotent re-grants. Banner-side affordances (FYI
        navigation, my-notes square) ride on Brief.kind=NOTE_UNLOCK.

        Line text dispatches by slug: admin-grant slugs (super-schizo,
        super-nomad) use the long "The administration recognizes…" format;
        every other slug uses the standard "Look!—new Note unlocked. {Note
        name} recognizes {username} the {title}." format. Both wrap the Note
        name in a `note-ref` anchor pointing at /billboard/my-notes/.
        Author is hardcoded to the seeded `adman` User; the per-line username
        column then attributes the Line correctly."""
        from django.utils import timezone

        from apps.billboard.models import Brief, Line, Post
        from apps.lyric.models import get_or_create_adman

        note, created = cls.objects.get_or_create(
            user=user, slug=slug,
            defaults={"earned_at": timezone.now()},
        )
        if not created:
            return note, created, None

        post, _ = Post.objects.get_or_create(
            owner=user, kind=Post.KIND_NOTE_UNLOCK,
            defaults={"title": NOTE_UNLOCK_POST_TITLE},
        )
        # Existing Note-unlock Posts (pre-0004 migration) might lack a title
        # if they predate this code path's get_or_create defaults. Heal once.
        if post.title != NOTE_UNLOCK_POST_TITLE:
            post.title = NOTE_UNLOCK_POST_TITLE
            post.save(update_fields=["title"])

        # Bare-email fallback when user.username is None (no `@` prefix —
        # the address already carries one). When username is set, use the
        # `@handle` form. Both wrapped in .post-attribution so the CSS
        # palette key (--quaUser) lights up the username + title combo.
        handle = f"@{user.username}" if user.username else user.email
        note_anchor = (
            f'<a class="note-ref" href="/billboard/my-notes/">'
            f'{note.display_name}</a>'
        )
        attr_handle = f'<span class="post-attribution">{handle}</span>'
        attr_title  = f'<span class="post-attribution">{note.display_title}</span>'
        if slug in _ADMIN_NOTE_SLUGS:
            line_text = (
                f"The administration recognizes {attr_handle} for {note_anchor}, "
                f"which bestows the honorary title of {attr_title}. "
                "This does not entail any additional corporate benefits."
            )
        else:
            # Inline attribution reads "{handle} the {Note name}", not the
            # don-able title. For most slugs the two coincide ("the Stargazer")
            # but Baltimorean's title is the navbar flair "Ard!" — "the Ard!"
            # reads oddly inline; "the Baltimorean" matches the Note name.
            attr_combo = (
                f'<span class="post-attribution">{handle} '
                f'the {note.display_name}</span>'
            )
            line_text = (
                f"Look!—new Note unlocked. {note_anchor} "
                f"recognizes {attr_combo}."
            )

        # Lazy get-or-create: TransactionTestCase flushes the migration-seeded
        # adman row, so tests that create superusers (which auto-grants
        # super-schizo + super-nomad via the User post_save signal) need a
        # safety net. Production migrations seed it once.
        adman = get_or_create_adman()
        line = Line.objects.create(
            post=post, text=line_text, author=adman,
            admin_solicited=True,
        )
        brief = Brief.objects.create(
            owner=user,
            post=post,
            line=line,
            kind=Brief.KIND_NOTE_UNLOCK,
            title=note.display_name,
        )
        return note, created, brief
