"""
TextChoices enums backing model field ``choices``.

The core domain enums (``OrphanCategory``, ``OrphanAction``) are
re-exported from ``policy.py`` for callers that don't need the
model layer.
"""

from __future__ import annotations

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


class IdentitySource(models.TextChoices):
    """Provenance of a ``DiscordIdentity`` row."""

    OPERATOR = "operator", _("Manually linked by operator")
    HISTORICAL = "historical", _("Migrated from a deleted DiscordUser row")


class OrphanCategory(models.TextChoices):
    """Classification result for a guild member during audit."""

    UNKNOWN_GUEST = "unknown_guest", _("Identity unknown to AA")
    LINKED_NO_PERM = (
        "linked_no_perm",
        _("Identity known but lacks access_perm"),
    )
    BOT_FILTERED = "bot_filtered", _("Filtered as a known bot account")


class OrphanAction(models.TextChoices):
    """Action policy applied to an orphan finding."""

    REPORT = "report", _("Report only — no changes")
    STRIP = "strip", _("Strip AA-managed roles")
    STRIP_KICK = "strip_kick", _("Strip AA-managed roles and kick")


class AuditRunState(models.TextChoices):
    """Lifecycle state of an ``AuditRun`` row."""

    PENDING = "pending", _("Pending")
    RUNNING = "running", _("Running")
    SUCCEEDED = "succeeded", _("Succeeded")
    FAILED = "failed", _("Failed")
    BUDGET_EXCEEDED = "budget_exceeded", _("Budget exceeded")
    ABANDONED = "abandoned", _("Abandoned")
    # Operator hit Ctrl-C / the worker received SIGTERM mid-run. Distinct
    # from FAILED (no exception was raised; the run was deliberately
    # cut short) and from ABANDONED (deliberate operator decision via
    # --abandon, not an interruption).
    INTERRUPTED = "interrupted", _("Interrupted")


class AuditFindingState(models.TextChoices):
    """Lifecycle state of an ``AuditFinding`` row."""

    PENDING = "pending", _("Pending")
    APPLIED = "applied", _("Applied")
    SKIPPED = "skipped", _("Skipped")
    FAILED = "failed", _("Failed")


class RunTrigger(models.TextChoices):
    """How the audit run / invocation was triggered."""

    CLI = "cli", _("Management command")
    BEAT = "beat", _("Celery beat schedule")
    MANUAL = "manual", _("Manual / API call")


class InvocationRefusal(models.TextChoices):
    """Reason an ``AuditInvocation`` was refused (``accepted=False``)."""

    RATE_LIMITED = "rate_limited", _("Per-user rate limit exceeded")
    # Permission check failed before reaching the rate-limit / quota
    # gate. Recorded so a malicious user repeatedly attempting an audit
    # leaves an audit-trail without consuming their own daily quota.
    PERMISSION_DENIED = "permission_denied", _("run_audit permission missing")
    # Destructive --action (strip / strip_kick) without --yes. Refused
    # rows do not count toward the per-user limit (matches the
    # rate-limited contract documented on AuditInvocation.accepted).
    CONFIRMATION_DECLINED = (
        "confirmation_declined",
        _("Destructive action requested without --yes"),
    )


class FindingSkipReason(models.TextChoices):
    """Reason an ``AuditFinding`` was skipped during the apply phase."""

    HTTP_ERROR = "http_error", _("Discord API HTTP error")
    PROTECTED_CONFIG = "protected_config", _("Member added to protect list")
    RECLASSIFIED = "reclassified", _("Member reclassified between phases")
    RECLASSIFIED_USER_CHANGE = (
        "reclassified_user_change",
        _("Linked AA user changed between phases"),
    )
    AUTH_FAILED = "auth_failed", _("Discord bot auth failed (401)")
    FORBIDDEN_403 = "forbidden_403", _("Discord refused the request (403)")
    # Operator marked the finding as reviewed via the admin bulk-action.
    # Distinct from PROTECTED_CONFIG: that one carries a uid forward
    # into every future run; MANUALLY_REVIEWED only retires *this*
    # finding, leaving classification of the same uid in the next run
    # unchanged.
    MANUALLY_REVIEWED = (
        "manually_reviewed",
        _("Operator marked the finding as manually reviewed"),
    )
    # build_plan and apply_plan disagreed on which findings exist
    # (typically: an admin deleted the AuditFinding row out-of-band
    # between the two phases). Distinct from HTTP_ERROR so admins can
    # filter forensic views; the in-memory AppliedAction is recorded
    # but no DB row is updated (there's nothing to update).
    INTERNAL_INCONSISTENCY = (
        "internal_inconsistency",
        _("Build/apply disagreed on which findings exist"),
    )


class ConfigAction(models.TextChoices):
    """Operation recorded by ``ConfigChangeLog`` for a tracked-model edit."""

    CREATE = "create", _("Created")
    UPDATE = "update", _("Updated")
    DELETE = "delete", _("Deleted")
