# Generated by Django 4.2.30 on 2026-05-14 13:21

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models

import aa_discord_audit.models._append_only


class Migration(migrations.Migration):
    initial = True

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]

    operations = [
        migrations.CreateModel(
            name="ProtectedDiscordMember",
            fields=[
                (
                    "id",
                    models.BigAutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                ("uid", models.PositiveBigIntegerField(unique=True)),
                ("reason", models.TextField()),
                ("added_at", models.DateTimeField(auto_now_add=True)),
                (
                    "added_by",
                    models.ForeignKey(
                        blank=True,
                        null=True,
                        on_delete=django.db.models.deletion.SET_NULL,
                        related_name="+",
                        to=settings.AUTH_USER_MODEL,
                    ),
                ),
            ],
            options={
                "verbose_name": "protected discord member",
                "verbose_name_plural": "protected discord members",
                "permissions": [
                    (
                        "manage_protected_member",
                        "Can edit ProtectedDiscordMember rows",
                    )
                ],
            },
        ),
        migrations.CreateModel(
            name="ManagedRoleException",
            fields=[
                (
                    "id",
                    models.BigAutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                ("role_id", models.PositiveBigIntegerField(unique=True)),
                ("role_name", models.CharField(max_length=128)),
                ("reason", models.TextField(blank=True)),
                ("created_at", models.DateTimeField(auto_now_add=True)),
                (
                    "created_by",
                    models.ForeignKey(
                        blank=True,
                        null=True,
                        on_delete=django.db.models.deletion.SET_NULL,
                        related_name="+",
                        to=settings.AUTH_USER_MODEL,
                    ),
                ),
            ],
            options={
                "verbose_name": "managed role exception",
                "verbose_name_plural": "managed role exceptions",
                "permissions": [
                    (
                        "manage_role_exception",
                        "Can edit ManagedRoleException rows",
                    )
                ],
            },
        ),
        migrations.CreateModel(
            name="InitialAuditAcknowledgement",
            fields=[
                (
                    "id",
                    models.BigAutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                ("acknowledged_at", models.DateTimeField(auto_now_add=True)),
                ("notes", models.TextField(blank=True)),
                (
                    "acknowledged_by",
                    models.ForeignKey(
                        blank=True,
                        null=True,
                        on_delete=django.db.models.deletion.SET_NULL,
                        related_name="+",
                        to=settings.AUTH_USER_MODEL,
                    ),
                ),
            ],
            options={
                "verbose_name": "initial audit acknowledgement",
                "verbose_name_plural": "initial audit acknowledgements",
                "permissions": [
                    (
                        "acknowledge_initial_audit",
                        "Can release the first-run dry-run lock",
                    )
                ],
            },
        ),
        migrations.CreateModel(
            name="ConfigChangeLog",
            fields=[
                (
                    "id",
                    models.BigAutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                (
                    "actor_user_id",
                    models.PositiveBigIntegerField(blank=True, null=True),
                ),
                ("actor_username", models.CharField(max_length=150)),
                ("model_name", models.CharField(max_length=100)),
                ("object_pk", models.CharField(max_length=100)),
                (
                    "action",
                    models.CharField(
                        choices=[
                            ("create", "Created"),
                            ("update", "Updated"),
                            ("delete", "Deleted"),
                        ],
                        max_length=32,
                    ),
                ),
                ("payload", models.JSONField(default=dict)),
                (
                    "created_at",
                    models.DateTimeField(auto_now_add=True, db_index=True),
                ),
            ],
            options={
                "verbose_name": "config change log entry",
                "verbose_name_plural": "config change log entries",
                "indexes": [
                    models.Index(
                        fields=["model_name", "object_pk"],
                        name="configchangelog_target_idx",
                    ),
                    models.Index(
                        fields=["actor_username"],
                        name="configchangelog_actor_idx",
                    ),
                ],
            },
            bases=(
                aa_discord_audit.models._append_only.AppendOnlyModelMixin,
                models.Model,
            ),
        ),
        migrations.CreateModel(
            name="BotAccountUid",
            fields=[
                (
                    "id",
                    models.BigAutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                ("uid", models.PositiveBigIntegerField(unique=True)),
                ("notes", models.TextField(blank=True)),
                ("added_at", models.DateTimeField(auto_now_add=True)),
                (
                    "added_by",
                    models.ForeignKey(
                        blank=True,
                        null=True,
                        on_delete=django.db.models.deletion.SET_NULL,
                        related_name="+",
                        to=settings.AUTH_USER_MODEL,
                    ),
                ),
            ],
            options={
                "verbose_name": "bot account uid",
                "verbose_name_plural": "bot account uids",
                "permissions": [
                    ("manage_bot_account_uid", "Can edit BotAccountUid rows")
                ],
            },
        ),
        migrations.CreateModel(
            name="AuditRun",
            fields=[
                (
                    "id",
                    models.BigAutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                (
                    "started_at",
                    models.DateTimeField(auto_now_add=True, db_index=True),
                ),
                ("finished_at", models.DateTimeField(blank=True, null=True)),
                (
                    "state",
                    models.CharField(
                        choices=[
                            ("pending", "Pending"),
                            ("running", "Running"),
                            ("succeeded", "Succeeded"),
                            ("failed", "Failed"),
                            ("budget_exceeded", "Budget exceeded"),
                            ("abandoned", "Abandoned"),
                            ("interrupted", "Interrupted"),
                        ],
                        default="pending",
                        max_length=32,
                    ),
                ),
                (
                    "triggered_by",
                    models.CharField(
                        choices=[
                            ("cli", "Management command"),
                            ("beat", "Celery beat schedule"),
                            ("manual", "Manual / API call"),
                        ],
                        max_length=32,
                    ),
                ),
                ("cli_argv", models.JSONField(blank=True, default=list)),
                ("confirmation_bypassed", models.BooleanField(default=False)),
                ("dry_run", models.BooleanField(default=False)),
                (
                    "first_run_lock_was_active",
                    models.BooleanField(default=False),
                ),
                ("policy_snapshot", models.JSONField(default=dict)),
                ("members_scanned", models.PositiveBigIntegerField(default=0)),
                ("findings_count", models.PositiveBigIntegerField(default=0)),
                ("actions_applied", models.PositiveBigIntegerField(default=0)),
                ("actions_skipped", models.PositiveBigIntegerField(default=0)),
                ("summary", models.JSONField(default=dict)),
                ("error_message", models.TextField(blank=True)),
                (
                    "idempotency_key",
                    models.CharField(
                        blank=True, max_length=64, null=True, unique=True
                    ),
                ),
                (
                    "triggered_by_user",
                    models.ForeignKey(
                        blank=True,
                        null=True,
                        on_delete=django.db.models.deletion.SET_NULL,
                        related_name="+",
                        to=settings.AUTH_USER_MODEL,
                    ),
                ),
            ],
            options={
                "verbose_name": "audit run",
                "verbose_name_plural": "audit runs",
                "permissions": [("run_audit", "Can trigger an audit run")],
                "get_latest_by": "started_at",
            },
            bases=(
                aa_discord_audit.models._append_only.AppendOnlyModelMixin,
                models.Model,
            ),
        ),
        migrations.CreateModel(
            name="AuditInvocation",
            fields=[
                (
                    "id",
                    models.BigAutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                (
                    "triggered_by",
                    models.CharField(
                        choices=[
                            ("cli", "Management command"),
                            ("beat", "Celery beat schedule"),
                            ("manual", "Manual / API call"),
                        ],
                        max_length=32,
                    ),
                ),
                (
                    "started_at",
                    models.DateTimeField(auto_now_add=True, db_index=True),
                ),
                ("cli_argv", models.JSONField(blank=True, default=list)),
                ("accepted", models.BooleanField()),
                (
                    "refusal_reason",
                    models.CharField(
                        blank=True,
                        choices=[
                            ("rate_limited", "Per-user rate limit exceeded"),
                            (
                                "permission_denied",
                                "run_audit permission missing",
                            ),
                            (
                                "confirmation_declined",
                                "Destructive action requested without --yes",
                            ),
                        ],
                        max_length=64,
                    ),
                ),
                (
                    "audit_run_pk",
                    models.PositiveBigIntegerField(
                        blank=True,
                        db_index=True,
                        help_text="Snapshot of audit_run_id at insertion time. Survives retention pruning of AuditRun, so forensic queries spanning >RETENTION_DAYS can still join through this column when ``audit_run`` has been NULL'd.",
                        null=True,
                    ),
                ),
                (
                    "audit_run",
                    models.ForeignKey(
                        blank=True,
                        help_text="Live FK to the produced AuditRun (set after acceptance). Goes NULL on retention prune of the run; see ``audit_run_pk`` for the post-prune forensic anchor.",
                        null=True,
                        on_delete=django.db.models.deletion.SET_NULL,
                        related_name="invocations",
                        to="aa_discord_audit.auditrun",
                    ),
                ),
                (
                    "triggered_by_user",
                    models.ForeignKey(
                        blank=True,
                        null=True,
                        on_delete=django.db.models.deletion.SET_NULL,
                        related_name="+",
                        to=settings.AUTH_USER_MODEL,
                    ),
                ),
            ],
            options={
                "verbose_name": "audit invocation",
                "verbose_name_plural": "audit invocations",
                "default_permissions": ("view",),
            },
            bases=(
                aa_discord_audit.models._append_only.AppendOnlyModelMixin,
                models.Model,
            ),
        ),
        migrations.CreateModel(
            name="AuditFinding",
            fields=[
                (
                    "id",
                    models.BigAutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                ("member_id", models.PositiveBigIntegerField()),
                ("member_username", models.CharField(max_length=64)),
                (
                    "category",
                    models.CharField(
                        choices=[
                            ("unknown_guest", "Identity unknown to AA"),
                            (
                                "linked_no_perm",
                                "Identity known but lacks access_perm",
                            ),
                            (
                                "bot_filtered",
                                "Filtered as a known bot account",
                            ),
                        ],
                        max_length=32,
                    ),
                ),
                (
                    "state",
                    models.CharField(
                        choices=[
                            ("pending", "Pending"),
                            ("applied", "Applied"),
                            ("skipped", "Skipped"),
                            ("failed", "Failed"),
                        ],
                        default="pending",
                        max_length=32,
                    ),
                ),
                (
                    "action",
                    models.CharField(
                        choices=[
                            ("report", "Report only — no changes"),
                            ("strip", "Strip AA-managed roles"),
                            ("strip_kick", "Strip AA-managed roles and kick"),
                        ],
                        max_length=32,
                    ),
                ),
                ("role_ids_removed", models.JSONField(default=list)),
                ("role_names_removed", models.JSONField(default=list)),
                ("kicked", models.BooleanField(default=False)),
                (
                    "skipped_reason",
                    models.CharField(
                        blank=True,
                        choices=[
                            ("http_error", "Discord API HTTP error"),
                            (
                                "protected_config",
                                "Member added to protect list",
                            ),
                            (
                                "reclassified",
                                "Member reclassified between phases",
                            ),
                            (
                                "reclassified_user_change",
                                "Linked AA user changed between phases",
                            ),
                            ("auth_failed", "Discord bot auth failed (401)"),
                            (
                                "forbidden_403",
                                "Discord refused the request (403)",
                            ),
                            (
                                "manually_reviewed",
                                "Operator marked the finding as manually reviewed",
                            ),
                            (
                                "internal_inconsistency",
                                "Build/apply disagreed on which findings exist",
                            ),
                        ],
                        max_length=64,
                    ),
                ),
                ("error_message", models.TextField(blank=True)),
                (
                    "aa_user",
                    models.ForeignKey(
                        blank=True,
                        null=True,
                        on_delete=django.db.models.deletion.SET_NULL,
                        related_name="discord_audit_findings",
                        to=settings.AUTH_USER_MODEL,
                    ),
                ),
                (
                    "run",
                    models.ForeignKey(
                        help_text="Findings co-die with their run. The only path to a Run delete is the retention sweep (``prune_audit_runs``); CASCADE keeps that prune atomic without a per-finding fan-out.",
                        on_delete=django.db.models.deletion.CASCADE,
                        related_name="findings",
                        to="aa_discord_audit.auditrun",
                    ),
                ),
            ],
            options={
                "verbose_name": "audit finding",
                "verbose_name_plural": "audit findings",
            },
            bases=(
                aa_discord_audit.models._append_only.AppendOnlyModelMixin,
                models.Model,
            ),
        ),
        migrations.CreateModel(
            name="PendingKick",
            fields=[
                (
                    "id",
                    models.BigAutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                ("uid", models.PositiveBigIntegerField(db_index=True)),
                ("aa_user_id", models.PositiveBigIntegerField()),
                ("created_at", models.DateTimeField(auto_now_add=True)),
                ("attempts", models.PositiveIntegerField(default=0)),
                (
                    "last_attempt_at",
                    models.DateTimeField(blank=True, null=True),
                ),
                ("last_error", models.TextField(blank=True)),
                ("completed_at", models.DateTimeField(blank=True, null=True)),
                (
                    "audit_finding",
                    models.OneToOneField(
                        on_delete=django.db.models.deletion.CASCADE,
                        related_name="pending_kick",
                        to="aa_discord_audit.auditfinding",
                    ),
                ),
            ],
            options={
                "verbose_name": "pending kick",
                "verbose_name_plural": "pending kicks",
                "indexes": [
                    models.Index(
                        fields=["completed_at"], name="pendingkick_status_idx"
                    )
                ],
            },
        ),
        migrations.CreateModel(
            name="FindingActionOverride",
            fields=[
                (
                    "id",
                    models.BigAutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                ("member_id", models.PositiveBigIntegerField(unique=True)),
                (
                    "action",
                    models.CharField(
                        choices=[
                            ("report", "Report only — no changes"),
                            ("strip", "Strip AA-managed roles"),
                            ("strip_kick", "Strip AA-managed roles and kick"),
                        ],
                        max_length=32,
                    ),
                ),
                ("reason", models.TextField()),
                ("expires_at", models.DateTimeField(blank=True, null=True)),
                ("added_at", models.DateTimeField(auto_now_add=True)),
                (
                    "added_by",
                    models.ForeignKey(
                        blank=True,
                        null=True,
                        on_delete=django.db.models.deletion.SET_NULL,
                        related_name="+",
                        to=settings.AUTH_USER_MODEL,
                    ),
                ),
            ],
            options={
                "verbose_name": "finding action override",
                "verbose_name_plural": "finding action overrides",
                "permissions": [
                    (
                        "manage_finding_override",
                        "Can edit FindingActionOverride rows",
                    )
                ],
                "indexes": [
                    models.Index(
                        fields=["expires_at"],
                        name="findingoverride_expiry_idx",
                    )
                ],
            },
        ),
        migrations.CreateModel(
            name="DiscordIdentity",
            fields=[
                (
                    "id",
                    models.BigAutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                ("uid", models.PositiveBigIntegerField()),
                (
                    "source",
                    models.CharField(
                        choices=[
                            ("operator", "Manually linked by operator"),
                            (
                                "historical",
                                "Migrated from a deleted DiscordUser row",
                            ),
                        ],
                        max_length=32,
                    ),
                ),
                (
                    "username_snapshot",
                    models.CharField(
                        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.",
                        max_length=64,
                    ),
                ),
                (
                    "display_name_snapshot",
                    models.CharField(
                        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.",
                        max_length=64,
                    ),
                ),
                ("notes", models.TextField(blank=True)),
                ("created_at", models.DateTimeField(auto_now_add=True)),
                (
                    "last_seen_in_guild_at",
                    models.DateTimeField(blank=True, db_index=True, null=True),
                ),
                (
                    "user",
                    models.ForeignKey(
                        null=True,
                        on_delete=django.db.models.deletion.SET_NULL,
                        related_name="discord_identities",
                        to=settings.AUTH_USER_MODEL,
                    ),
                ),
            ],
            options={
                "verbose_name": "discord identity",
                "verbose_name_plural": "discord identities",
                "permissions": [
                    (
                        "manage_discord_identity",
                        "Can create or edit DiscordIdentity rows",
                    )
                ],
                "get_latest_by": "created_at",
                "indexes": [
                    models.Index(fields=["uid"], name="discord_aud_uid_idx")
                ],
            },
        ),
        migrations.AddConstraint(
            model_name="discordidentity",
            constraint=models.UniqueConstraint(
                fields=("uid", "user"),
                name="aa_discord_audit_discordidentity_uniq_uid_user",
            ),
        ),
        migrations.AddConstraint(
            model_name="auditrun",
            constraint=models.CheckConstraint(
                check=models.Q(
                    (
                        "state__in",
                        [
                            "pending",
                            "running",
                            "succeeded",
                            "failed",
                            "budget_exceeded",
                            "abandoned",
                            "interrupted",
                        ],
                    )
                ),
                name="aa_discord_audit_auditrun_state_valid",
            ),
        ),
        migrations.AddIndex(
            model_name="auditfinding",
            index=models.Index(
                fields=["run", "state"], name="auditfinding_run_state_idx"
            ),
        ),
        migrations.AddIndex(
            model_name="auditfinding",
            index=models.Index(
                fields=["member_id"], name="auditfinding_member_idx"
            ),
        ),
        migrations.AddConstraint(
            model_name="auditfinding",
            constraint=models.CheckConstraint(
                check=models.Q(
                    ("state__in", ["pending", "applied", "skipped", "failed"])
                ),
                name="aa_discord_audit_auditfinding_state_valid",
            ),
        ),
        migrations.AddConstraint(
            model_name="auditfinding",
            constraint=models.UniqueConstraint(
                fields=("run", "member_id"),
                name="aa_discord_audit_auditfinding_uniq_run_member",
            ),
        ),
    ]
