"""
Operator-managed configuration models.

The five tables here are edited by operators through Django admin
and, for four of them, drive ``classify()`` / ``apply_plan``
precedence:

- ``BotAccountUid``: uids skipped as bot accounts.
- ``ProtectedDiscordMember``: uids the audit must never act on.
- ``ManagedRoleException``: roles excluded from the AA-managed set.
- ``FindingActionOverride``: per-uid override of the resolved
  action (carried over only at the ``apply_plan`` layer; does not
  affect classification).
- ``InitialAuditAcknowledgement``: one-row sentinel that releases
  the first-run dry-run lock.

The first four use ``BulkBlockingManager`` so ``post_save`` /
``post_delete`` fire for every edit and the ``ConfigChangeLog`` trail
stays complete; ``InitialAuditAcknowledgement`` is a single-row
sentinel and uses the default manager.
"""

from __future__ import annotations

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

from ._append_only import BulkBlockingManager
from ._constants import DISCORD_ROLE_NAME_MAX_LENGTH, ENUM_MAX_LENGTH
from ._enums import OrphanAction


class BotAccountUid(models.Model):
    """
    Operator-managed list of Discord uids known to be bot accounts.

    Replaces the missing ``User.bot`` payload field from AA core's
    ``User`` dataclass. ``classify()`` returns
    ``OrphanCategory.BOT_FILTERED`` for members whose uid appears
    here, producing a finding row so the audit trail records what
    was filtered (defangs the silent-defang vector).
    """

    uid = models.PositiveBigIntegerField(unique=True)
    notes = models.TextField(blank=True)
    added_at = models.DateTimeField(auto_now_add=True)
    added_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )

    objects = BulkBlockingManager()

    class Meta:
        verbose_name = _("bot account uid")
        verbose_name_plural = _("bot account uids")
        permissions = [
            ("manage_bot_account_uid", "Can edit BotAccountUid rows"),
        ]

    def __str__(self) -> str:
        return f"BotAccountUid(uid={self.uid})"


class ProtectedDiscordMember(models.Model):
    """
    Members the audit must never act on.

    Admin-managed so changes can land without a redeploy during
    incidents.
    """

    uid = models.PositiveBigIntegerField(unique=True)
    reason = models.TextField()
    added_at = models.DateTimeField(auto_now_add=True)
    added_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )

    objects = BulkBlockingManager()

    class Meta:
        verbose_name = _("protected discord member")
        verbose_name_plural = _("protected discord members")
        permissions = [
            (
                "manage_protected_member",
                "Can edit ProtectedDiscordMember rows",
            ),
        ]

    def __str__(self) -> str:
        return f"ProtectedDiscordMember(uid={self.uid})"


class ManagedRoleException(models.Model):
    """
    Roles excluded from the AA-managed set despite name match.

    The single configuration knob for managed-role discovery.
    """

    role_id = models.PositiveBigIntegerField(unique=True)
    role_name = models.CharField(max_length=DISCORD_ROLE_NAME_MAX_LENGTH)
    reason = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )

    objects = BulkBlockingManager()

    class Meta:
        verbose_name = _("managed role exception")
        verbose_name_plural = _("managed role exceptions")
        permissions = [
            ("manage_role_exception", "Can edit ManagedRoleException rows"),
        ]

    def __str__(self) -> str:
        return (
            f"ManagedRoleException(role_id={self.role_id}, "
            f"name={self.role_name!r})"
        )


class FindingActionOverride(models.Model):
    """
    Per-uid override of the resolved ``OrphanAction``.

    Sits one layer above ``ProtectedDiscordMember`` semantically:
    where ``ProtectedDiscordMember`` always coerces an action to
    ``REPORT``, ``FindingActionOverride`` lets the operator pin
    *any* action for a specific uid, optionally with an expiry.
    The ``apply_plan`` lookup order is documented in
    ``_apply_finding``: ``ProtectedDiscordMember`` first (always
    REPORT, no expiry), then ``FindingActionOverride`` (typed
    action, optional expiry), then the policy default.

    Unique on ``member_id`` so the operator UI shows one row per
    uid; mutating the row records a ``ConfigChangeLog`` entry via
    ``signals.py``.
    """

    member_id = models.PositiveBigIntegerField(unique=True)
    action = models.CharField(
        max_length=ENUM_MAX_LENGTH, choices=OrphanAction.choices
    )
    reason = models.TextField()
    expires_at = models.DateTimeField(null=True, blank=True)
    added_at = models.DateTimeField(auto_now_add=True)
    added_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )

    objects = BulkBlockingManager()

    class Meta:
        verbose_name = _("finding action override")
        verbose_name_plural = _("finding action overrides")
        permissions = [
            (
                "manage_finding_override",
                "Can edit FindingActionOverride rows",
            ),
        ]
        # Hot path on apply: ``build_plan`` snapshots every active
        # override into ``AuditPlan.override_by_uid`` via a single
        # query filtered by the expiry window. The index supports
        # both the ``expires_at IS NULL`` and the
        # ``expires_at > now`` predicates.
        indexes = [
            models.Index(
                fields=["expires_at"],
                name="findingoverride_expiry_idx",
            ),
        ]

    def __str__(self) -> str:
        return (
            f"FindingActionOverride(member_id={self.member_id}, "
            f"action={self.action})"
        )


class InitialAuditAcknowledgement(models.Model):
    """
    One-row sentinel that releases the first-run dry-run lock.

    Creating a row requires both ``run_audit`` and
    ``acknowledge_initial_audit`` permissions; the admin's
    ``has_add_permission`` AND-gates them. Deletion is blocked at
    the admin layer; operators wanting to re-arm the lock must
    drop the row via shell, which is intentional friction.
    """

    acknowledged_at = models.DateTimeField(auto_now_add=True)
    acknowledged_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    notes = models.TextField(blank=True)

    class Meta:
        verbose_name = _("initial audit acknowledgement")
        verbose_name_plural = _("initial audit acknowledgements")
        permissions = [
            (
                "acknowledge_initial_audit",
                "Can release the first-run dry-run lock",
            ),
        ]

    def __str__(self) -> str:
        return (
            f"InitialAuditAcknowledgement"
            f"(at={self.acknowledged_at.isoformat()})"
        )
