"""
Identity-mapping model.

``DiscordIdentity`` is *not* append-only — it is a mutable
observation map and supports plain ORM updates (see
``audit/_apply.py`` where ``last_seen_in_guild_at`` is bumped via
``DiscordIdentity.objects.filter(uid__in=…).update(...)``).
"""

from __future__ import annotations

from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _

from ._constants import DISCORD_NAME_MAX_LENGTH, ENUM_MAX_LENGTH
from ._enums import IdentitySource


class DiscordIdentity(models.Model):
    """
    Observation-only mapping of Discord uid -> AA user.

    Distinct from ``DiscordUser`` which is the active service contract.
    """

    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        related_name="discord_identities",
    )
    uid = models.PositiveBigIntegerField()
    source = models.CharField(
        max_length=ENUM_MAX_LENGTH, choices=IdentitySource.choices
    )
    # Snapshots: populated at row creation, never refreshed afterwards.
    # Discord username/global_name are mutable on Discord's side, so
    # these fields freeze the human-readable handle at capture time
    # for forensic admin display.
    username_snapshot = models.CharField(
        max_length=DISCORD_NAME_MAX_LENGTH,
        blank=True,
        help_text=_(
            "Discord username captured at row creation. HISTORICAL rows "
            "are populated by the post_delete signal; OPERATOR rows are "
            "filled by the admin. Not refreshed if the user renames."
        ),
    )
    display_name_snapshot = models.CharField(
        max_length=DISCORD_NAME_MAX_LENGTH,
        blank=True,
        help_text=_(
            "Discord display name (global_name) captured at row creation. "
            "AA core's DiscordUser does not store this, so HISTORICAL "
            "rows leave it blank; OPERATOR rows or audit-time enrichment "
            "may fill it. Not refreshed if the user changes display name."
        ),
    )
    notes = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    last_seen_in_guild_at = models.DateTimeField(
        null=True, blank=True, db_index=True
    )

    class Meta:
        # Use ``get_latest_by`` instead of ``ordering=("-created_at",)``
        # so ``DiscordIdentity.objects.latest()`` works without leaking
        # an implicit ORDER BY into ``count()`` / ``aggregate()`` /
        # ``distinct()`` querysets. Admin / UI callers that need a
        # specific order pass ``order_by(...)`` explicitly. Mirrors
        # the AuditRun.Meta decision.
        get_latest_by = "created_at"
        verbose_name = _("discord identity")
        verbose_name_plural = _("discord identities")
        constraints = [
            # The pair ``(uid, user)`` is unique. PostgreSQL, MySQL and
            # SQLite all treat NULL as distinct in unique indexes, so
            # multiple rows with ``user=NULL`` for the same uid remain
            # legal as forensic gravestones after SET_NULL cascades.
            # NULL-user gravestones are intentionally non-unique —
            # SET_NULL cascade can produce several over time as the
            # same uid is re-linked and re-orphaned — and duplicate
            # prevention for *live* rows rests on this constraint
            # together with the ``user_id is None`` short-circuit in
            # ``signals.record_historical_identity``: the constraint
            # alone cannot dedupe NULL-user rows, and the signal
            # alone cannot prevent two parallel writers; they form
            # the two halves of the invariant.
            models.UniqueConstraint(
                fields=["uid", "user"],
                name="aa_discord_audit_discordidentity_uniq_uid_user",
            ),
        ]
        indexes = [models.Index(fields=["uid"], name="discord_aud_uid_idx")]
        permissions = [
            (
                "manage_discord_identity",
                _("Can create or edit DiscordIdentity rows"),
            ),
        ]

    def __str__(self) -> str:
        return (
            f"DiscordIdentity(uid={self.uid}, user={self.user_id}, "
            f"source={self.source})"
        )
