"""Abstract base models reused by all business entities.

These are deliberately small. Domain-specific mixins live next to their models.
"""

from __future__ import annotations

import uuid
from typing import ClassVar

from django.db import models
from django.utils.translation import gettext_lazy as _

from simorgh.core.db.scoped import (
    ScopedManager,
    ScopedSoftDeleteManager,
    TenantWideScopedManager,
)
from simorgh.core.models.soft_delete import (
    SoftDeleteManager,
    SoftDeleteModel,
    SoftDeleteQuerySet,
)
from simorgh.core.models.versioned import VersionedModel, retry_on_conflict

__all__ = [
    "AuditedModel",
    "OrderedModel",
    "retry_on_conflict",
    "ScopedManager",
    "ScopedSoftDeleteManager",
    "SoftDeleteManager",
    "SoftDeleteModel",
    "SoftDeleteQuerySet",
    "TenantScopedModel",
    "TimeStampedModel",
    "UUIDModel",
    "VersionedModel",
]


class TimeStampedModel(models.Model):
    """Adds created/updated timestamps to a model."""

    created_at = models.DateTimeField(_("created at"), auto_now_add=True, db_index=True)
    updated_at = models.DateTimeField(_("updated at"), auto_now=True)

    class Meta:
        abstract = True


class UUIDModel(models.Model):
    """Use a UUID as the public identifier alongside the integer PK."""

    public_id = models.UUIDField(
        _("public id"),
        default=uuid.uuid4,
        editable=False,
        unique=True,
        db_index=True,
    )

    class Meta:
        abstract = True


class AuditedModel(models.Model):
    """Adds created_by / updated_by tracking, auto-populated from RequestContext.

    Place this mixin on any entity where actor attribution is required.
    Both fields are nullable so management commands and Celery tasks that run
    without a bound RequestContext work without errors — the fields simply stay
    NULL when no actor is available.

    If the caller passes an explicit ``update_fields`` list, ``created_by``
    (on insert) and ``updated_by`` (on every save) are appended automatically
    so the DB write remains consistent with the in-memory state.
    """

    created_by = models.ForeignKey(
        "accounts.User",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
        verbose_name=_("created by"),
        editable=False,
    )
    updated_by = models.ForeignKey(
        "accounts.User",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
        verbose_name=_("updated by"),
        editable=False,
    )

    class Meta:
        abstract = True

    def save(
        self,
        force_insert: bool = False,
        force_update: bool = False,
        using: str | None = None,
        update_fields: list[str] | tuple[str, ...] | None = None,
    ) -> None:
        from simorgh.core.context import current_request_context

        ctx = current_request_context()
        actor_id = ctx.actor.pk if ctx.actor and getattr(ctx.actor, "pk", None) else None

        if actor_id:
            is_adding = self._state.adding
            if is_adding and not self.created_by_id:
                self.created_by_id = actor_id
            self.updated_by_id = actor_id

            # Keep update_fields consistent with the in-memory assignment above.
            if update_fields is not None:
                extra: set[str] = {"updated_by"}
                if is_adding:
                    extra.add("created_by")
                update_fields = list(set(update_fields) | extra)

        super().save(
            force_insert=force_insert,
            force_update=force_update,
            using=using,
            update_fields=update_fields,
        )


class OrderedModel(models.Model):
    """Mixin that adds a ``sort_order`` field for drag-and-drop or manual ordering.

    ``sort_order`` is a non-unique positive integer so sibling rows can share
    a rank level.  Callers are responsible for maintaining uniqueness within a
    scope (e.g. parent + tenant) if required.

    The default ``Meta.ordering`` is ``["sort_order"]``; override it in the
    concrete model if a different ordering is needed.

    Usage::

        class MenuItem(OrderedModel, TenantScopedModel):
            parent = models.ForeignKey("self", null=True, ...)
    """

    sort_order = models.PositiveIntegerField(
        _("sort order"),
        default=0,
        db_index=True,
    )

    class Meta:
        abstract = True
        ordering = ["sort_order"]


class TenantScopedModel(TimeStampedModel):
    """Base for every business entity that belongs to a tenant + org node.

    Subclasses inherit:
      * ``tenant`` FK → `tenants.Tenant`
      * ``organization_node`` FK → `organizations.OrganizationNode`
      * ``objects`` manager exposing `.scoped()` / `.scoped_for(ctx)` that
        injects tenant + hierarchy filters from the request context.

    Never call ``Model.objects.all()`` on subclasses in business code — use
    `.scoped()`. The manager warns in DEBUG when you do.
    """

    tenant = models.ForeignKey(
        "tenants.Tenant",
        on_delete=models.CASCADE,
        related_name="+",
        verbose_name=_("tenant"),
    )
    organization_node = models.ForeignKey(
        "organizations.OrganizationNode",
        on_delete=models.PROTECT,
        related_name="+",
        verbose_name=_("organization node"),
    )

    objects = ScopedManager()

    class Meta:
        abstract = True
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=("tenant", "organization_node")),
        ]
