"""
``prune_audit_runs`` management command.

Manual entry point for retention pruning. Two ordered phases:

1. **Expire idempotency keys** — NULL-ify ``idempotency_key`` on
   runs older than ``AA_DISCORD_AUDIT_IDEMPOTENCY_KEY_TTL_DAYS`` so
   the dedup token can be reused while the run row itself remains.
   Skipped when the TTL setting is ``0`` (the default).
2. **Delete aged rows** — delete ``AuditRun`` rows older than
   ``AA_DISCORD_AUDIT_RUN_RETENTION_DAYS``; ``AuditFinding`` cascades.
   Skipped when retention is opted-out (sentinel ``0``).

``--dry-run`` reports both phases without writing. ``--keys-only``
executes Phase 1 and stops; the row-delete phase is bypassed.
"""

from __future__ import annotations

from datetime import datetime, timedelta
from typing import TYPE_CHECKING

from django.core.management.base import BaseCommand
from django.utils import timezone

from ..._log import get_logger
from ..._metrics import observe_runs_pruned
from ...app_settings import (
    AA_DISCORD_AUDIT_IDEMPOTENCY_KEY_TTL_DAYS,
    AA_DISCORD_AUDIT_RUN_RETENTION_DAYS,
)
from ...models import AuditRun
from ...persistence import expire_idempotency_keys, log_pending_purges

if TYPE_CHECKING:  # pragma: no cover
    import argparse

logger = get_logger(__name__)


class Command(BaseCommand):
    help = (
        "Two-phase retention: (1) NULL-ify idempotency keys older than "
        "AA_DISCORD_AUDIT_IDEMPOTENCY_KEY_TTL_DAYS, then (2) delete "
        "AuditRun rows older than AA_DISCORD_AUDIT_RUN_RETENTION_DAYS. "
        "AuditFinding rows cascade. InitialAuditAcknowledgement is "
        "never pruned regardless of age."
    )

    def add_arguments(self, parser: argparse.ArgumentParser) -> None:
        parser.add_argument(
            "--dry-run",
            action="store_true",
            help="Report counts for both phases without writing.",
        )
        parser.add_argument(
            "--keys-only",
            action="store_true",
            help=(
                "Execute Phase 1 (idempotency-key expiry) only; skip "
                "row deletion. Combines with --dry-run."
            ),
        )

    def handle(self, *args, **options) -> None:
        now = timezone.now()
        dry_run = options["dry_run"]

        self._phase_expire_keys(now=now, dry_run=dry_run)

        if options["keys_only"]:
            return

        self._phase_delete_rows(now=now, dry_run=dry_run)

    def _phase_expire_keys(self, *, now: datetime, dry_run: bool) -> None:
        if AA_DISCORD_AUDIT_IDEMPOTENCY_KEY_TTL_DAYS == 0:
            self.stdout.write(
                self.style.NOTICE(
                    "AA_DISCORD_AUDIT_IDEMPOTENCY_KEY_TTL_DAYS=0: "
                    "idempotency-key expiry disabled."
                )
            )
            return

        cutoff = now - timedelta(
            days=AA_DISCORD_AUDIT_IDEMPOTENCY_KEY_TTL_DAYS
        )
        queryset = AuditRun.objects.filter(
            started_at__lt=cutoff,
            idempotency_key__isnull=False,
        )

        if dry_run:
            count = queryset.count()
            self.stdout.write(
                self.style.NOTICE(
                    f"--dry-run: {count} idempotency keys older than "
                    f"{cutoff.isoformat()} would be cleared."
                )
            )
            return

        cleared = expire_idempotency_keys(cutoff)
        self.stdout.write(
            self.style.SUCCESS(
                f"Cleared {cleared} idempotency keys older than "
                f"{cutoff.isoformat()}."
            )
        )

    def _phase_delete_rows(self, *, now: datetime, dry_run: bool) -> None:
        # 0 is the indefinite-retention sentinel; opt-out gated by
        # AA_DISCORD_AUDIT_RETENTION_OPT_OUT_ACKNOWLEDGED at startup.
        if AA_DISCORD_AUDIT_RUN_RETENTION_DAYS == 0:
            self.stdout.write(
                self.style.WARNING(
                    "AA_DISCORD_AUDIT_RUN_RETENTION_DAYS=0: retention is "
                    "explicitly opted out "
                    "(RETENTION_OPT_OUT_ACKNOWLEDGED=True). "
                    "Nothing to prune."
                )
            )
            return

        cutoff = now - timedelta(days=AA_DISCORD_AUDIT_RUN_RETENTION_DAYS)
        queryset = AuditRun.objects.filter(started_at__lt=cutoff)

        if dry_run:
            count = queryset.count()
            self.stdout.write(
                self.style.NOTICE(
                    f"--dry-run: {count} AuditRun rows older than "
                    f"{cutoff.isoformat()} would be deleted."
                )
            )
            return

        # PENDING tombstone preamble — symmetric with the Celery task.
        log_pending_purges(queryset)

        # ``QuerySet.delete()`` returns ``(total_deleted, {label: count})``;
        # ``breakdown[AuditRun._meta.label]`` is the parent-level count
        # the metric tracks, so the previous extra ``queryset.count()``
        # round-trip is dropped on the non-dry-run path. ``deleted`` is
        # the cascade total (AuditRun + AuditFinding) and stays in the
        # operator-facing summary line.
        deleted, breakdown = queryset.delete()
        parent_count = breakdown.get(AuditRun._meta.label, 0)
        observe_runs_pruned(parent_count)
        self.stdout.write(
            self.style.SUCCESS(f"Pruned {deleted} rows ({breakdown})")
        )
