"""Soft-delete mixin and queryset.

Models inheriting :class:`SoftDeleteModel` keep rows in the database after
``delete()`` is called; ``is_deleted`` flips to ``True`` and ``deleted_at`` /
``deleted_by`` are populated. The default manager filters them out so business
code never sees tombstones unless it asks explicitly via ``with_deleted()``.

Cascade behaviour:
- ``soft_cascade`` (class attribute, list of related accessor names) marks
  reverse relations that should also be soft-deleted recursively.
- Hard delete is still available via :meth:`hard_delete`.
"""

from __future__ import annotations

from collections.abc import Iterable
from typing import ClassVar

from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

from simorgh.core.context import current_request_context


class SoftDeleteQuerySet(models.QuerySet):
    """Three views on a soft-delete-aware table."""

    def alive(self) -> SoftDeleteQuerySet:
        return self.filter(is_deleted=False)

    def dead(self) -> SoftDeleteQuerySet:
        return self.filter(is_deleted=True)

    def with_deleted(self) -> SoftDeleteQuerySet:
        return self.all()

    def delete(self) -> tuple[int, dict[str, int]]:  # type: ignore[override]
        """Bulk soft delete — flips flag on every matched row.

        Note: bulk soft-delete does **not** cascade across reverse relations;
        call :meth:`SoftDeleteModel.delete` instance-by-instance when you need
        cascades.
        """

        ctx = current_request_context()
        actor_id = ctx.actor.pk if ctx.actor and getattr(ctx.actor, "pk", None) else None
        count = self.update(
            is_deleted=True,
            deleted_at=timezone.now(),
            deleted_by_id=actor_id,
        )
        return count, {self.model._meta.label: count}

    def hard_delete(self) -> tuple[int, dict[str, int]]:
        """Bypass soft-delete — actually remove rows from the DB."""

        return super().delete()


class SoftDeleteManager(models.Manager.from_queryset(SoftDeleteQuerySet)):
    """Manager that hides deleted rows by default."""

    def get_queryset(self) -> SoftDeleteQuerySet:  # type: ignore[override]
        return super().get_queryset().filter(is_deleted=False)

    def with_deleted(self) -> SoftDeleteQuerySet:
        return super().get_queryset()

    def only_deleted(self) -> SoftDeleteQuerySet:
        return super().get_queryset().filter(is_deleted=True)


class SoftDeleteModel(models.Model):
    """Mixin that makes ``delete()`` mark a row as deleted instead of removing it."""

    is_deleted = models.BooleanField(_("deleted"), default=False, db_index=True)
    deleted_at = models.DateTimeField(_("deleted at"), null=True, blank=True)
    deleted_by = models.ForeignKey(
        "accounts.User",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
        verbose_name=_("deleted by"),
    )

    # Subclasses may set this to a tuple of reverse accessor names that should
    # be soft-deleted recursively. Example: ``soft_cascade = ("items", "lines")``.
    soft_cascade: ClassVar[tuple[str, ...]] = ()

    class Meta:
        abstract = True

    def delete(  # type: ignore[override]
        self,
        using: str | None = None,
        keep_parents: bool = False,
        *,
        actor_id: int | None = None,
    ) -> tuple[int, dict[str, int]]:
        ctx = current_request_context()
        if actor_id is None and ctx.actor and getattr(ctx.actor, "pk", None):
            actor_id = ctx.actor.pk

        if self.is_deleted:
            return 0, {}

        self.is_deleted = True
        self.deleted_at = timezone.now()
        self.deleted_by_id = actor_id
        self.save(update_fields=["is_deleted", "deleted_at", "deleted_by"], using=using)

        total = 1
        breakdown: dict[str, int] = {self._meta.label: 1}
        for accessor in self.soft_cascade:
            related = getattr(self, accessor, None)
            if related is None:
                continue
            qs = related.all() if hasattr(related, "all") else related
            for child in _iter_qs(qs):
                if isinstance(child, SoftDeleteModel) and not child.is_deleted:
                    c_total, c_breakdown = child.delete(using=using, actor_id=actor_id)
                    total += c_total
                    for k, v in c_breakdown.items():
                        breakdown[k] = breakdown.get(k, 0) + v
        return total, breakdown

    def hard_delete(self, using: str | None = None, keep_parents: bool = False) -> tuple[
        int, dict[str, int]
    ]:
        return super().delete(using=using, keep_parents=keep_parents)

    def restore(self, *, actor_id: int | None = None) -> None:
        """Undo a previous soft-delete."""

        if not self.is_deleted:
            return
        self.is_deleted = False
        self.deleted_at = None
        self.deleted_by_id = None
        self.save(update_fields=["is_deleted", "deleted_at", "deleted_by"])


def _iter_qs(qs: object) -> Iterable[object]:
    if hasattr(qs, "all"):
        return list(qs.all())  # type: ignore[no-any-return]
    if isinstance(qs, list | tuple):
        return qs
    return [qs]


__all__ = ["SoftDeleteManager", "SoftDeleteModel", "SoftDeleteQuerySet"]
