"""Scoped query engine — tenant + hierarchy + permission filter injection.

Models inheriting `TenantScopedModel` (see `simorgh.core.models`) get a
`ScopedManager` exposed as `Model.objects`. Two ways to read:

  * ``Model.objects.scoped()``  — uses `current_request_context()`.
  * ``Model.objects.scoped_for(ctx)`` — explicit context (jobs, tests).

`Model.objects.all()` still works for admin/migrations but emits a
``DeprecationWarning`` in DEBUG to nudge callers toward the scoped API.

`ScopedSoftDeleteManager` is the combined manager for models that extend both
`TenantScopedModel` and `SoftDeleteModel`. It applies both the soft-delete
filter (``is_deleted=False`` by default) and the tenant+hierarchy scope when
``.scoped()`` / ``.scoped_for(ctx)`` is called.
"""

from __future__ import annotations

import warnings

from django.conf import settings
from django.db import models

from simorgh.core.context import RequestContext, current_request_context


class ScopedQuerySet(models.QuerySet):
    def for_context(self, ctx: RequestContext) -> ScopedQuerySet:
        if ctx.is_superuser:
            # Superuser bypasses scoping but still respects tenant if one is bound.
            if ctx.tenant is not None:
                return self.filter(tenant_id=ctx.tenant.pk)
            return self
        if not ctx.is_authenticated or ctx.tenant is None:
            return self.none()
        qs = self.filter(tenant_id=ctx.tenant.pk)
        if ctx.org_node_ids:
            qs = qs.filter(organization_node_id__in=ctx.org_node_ids)
        else:
            return qs.none()
        return qs


class ScopedManager(models.Manager.from_queryset(ScopedQuerySet)):
    """Default manager for `TenantScopedModel`.

    Adds `.scoped()` / `.scoped_for(ctx)` and warns when `.all()` is used
    without an explicit scope in DEBUG.
    """

    def scoped(self) -> ScopedQuerySet:
        return self.get_queryset().for_context(current_request_context())

    def scoped_for(self, ctx: RequestContext) -> ScopedQuerySet:
        return self.get_queryset().for_context(ctx)

    def all(self) -> ScopedQuerySet:  # type: ignore[override]
        if getattr(settings, "DEBUG", False) and getattr(settings, "SCOPED_QUERY_STRICT", True):
            warnings.warn(
                f"{self.model.__name__}.objects.all() called on a tenant-scoped model "
                "— use `.scoped()` or `.scoped_for(ctx)` instead.",
                category=DeprecationWarning,
                stacklevel=2,
            )
        return super().all()


# ---------------------------------------------------------------------------
# Combined: scoped + soft-delete
# ---------------------------------------------------------------------------

class ScopedSoftDeleteQuerySet(ScopedQuerySet):
    """QuerySet combining tenant-hierarchy scoping with soft-delete views.

    Inherits ``for_context()`` from ``ScopedQuerySet`` and adds the two
    explicit soft-delete filters: ``alive()`` and ``dead()``.

    Note: ``with_deleted()`` is intentionally only on the *manager*, not the
    queryset, because Django querysets do not support removing previously
    applied filters.  Use ``Model.objects.with_deleted()`` to start a
    fresh un-filtered queryset.

        Model.objects.scoped()                    # alive + scoped
        Model.objects.with_deleted()              # all rows, no scope
        Model.objects.with_deleted().for_context(ctx)  # all rows + scoped
    """

    def alive(self) -> ScopedSoftDeleteQuerySet:
        return self.filter(is_deleted=False)

    def dead(self) -> ScopedSoftDeleteQuerySet:
        return self.filter(is_deleted=True)


class ScopedSoftDeleteManager(models.Manager.from_queryset(ScopedSoftDeleteQuerySet)):
    """Manager for entities that require BOTH tenant isolation and soft-delete.

    Use this on any model that inherits from both ``TenantScopedModel`` and
    ``SoftDeleteModel``.  Replaces the conflicting pair of
    ``ScopedManager`` + ``SoftDeleteManager`` that previously required
    choosing one at the expense of the other.

    Default queryset filters ``is_deleted=False``.

    API:
      * ``.scoped()``           — alive rows scoped to request context
      * ``.scoped_for(ctx)``    — alive rows scoped to explicit context
      * ``.with_deleted()``     — ALL rows (no ``is_deleted`` filter)
      * ``.only_deleted()``     — only soft-deleted rows
      * ``.all()``              — alive rows (warns in DEBUG)
    """

    def get_queryset(self) -> ScopedSoftDeleteQuerySet:  # type: ignore[override]
        return super().get_queryset().filter(is_deleted=False)

    def scoped(self) -> ScopedSoftDeleteQuerySet:
        return self.get_queryset().for_context(current_request_context())

    def scoped_for(self, ctx: RequestContext) -> ScopedSoftDeleteQuerySet:
        return self.get_queryset().for_context(ctx)

    def with_deleted(self) -> ScopedSoftDeleteQuerySet:
        """Return ALL rows, bypassing the ``is_deleted=False`` filter."""
        return super().get_queryset()  # type: ignore[return-value]

    def only_deleted(self) -> ScopedSoftDeleteQuerySet:
        """Return only soft-deleted rows (admin / restore tooling)."""
        return super().get_queryset().filter(is_deleted=True)  # type: ignore[return-value]

    def all(self) -> ScopedSoftDeleteQuerySet:  # type: ignore[override]
        if getattr(settings, "DEBUG", False) and getattr(settings, "SCOPED_QUERY_STRICT", True):
            warnings.warn(
                f"{self.model.__name__}.objects.all() called on a scoped+soft-delete model "
                "— use `.scoped()` or `.scoped_for(ctx)` instead.",
                category=DeprecationWarning,
                stacklevel=2,
            )
        return super().all()  # type: ignore[return-value]


# ---------------------------------------------------------------------------
# Tenant-wide scoping: allows NULL org_node (reference / config data)
# ---------------------------------------------------------------------------


class TenantWideScopedQuerySet(ScopedQuerySet):
    """QuerySet that includes rows with NULL organisation_node.

    Use for reference-data entities (LeaveType, PublicHoliday, ShiftDefinition
    etc.) that are tenant-wide and not scoped to a specific org node.

    For authenticated users within a tenant, returns rows matching their
    org-node membership **OR** rows where ``organization_node IS NULL``
    (tenant-wide reference data).  For unauthenticated / no-tenant, returns
    ``.none()``.
    """

    def for_context(self, ctx: RequestContext) -> TenantWideScopedQuerySet:
        if ctx.is_superuser:
            if ctx.tenant is not None:
                return self.filter(tenant_id=ctx.tenant.pk)
            return self
        if not ctx.is_authenticated or ctx.tenant is None:
            return self.none()
        qs = self.filter(tenant_id=ctx.tenant.pk)
        if ctx.org_node_ids:
            return qs.filter(
                models.Q(organization_node_id__in=ctx.org_node_ids)
                | models.Q(organization_node__isnull=True)
            )
        # User has tenant but no org-node memberships — show only tenant-wide data.
        return qs.filter(organization_node__isnull=True)


class TenantWideScopedManager(ScopedManager.from_queryset(TenantWideScopedQuerySet)):
    """Manager for tenant-scoped entities where NULL org_node is allowed.

    Use this on reference-data models that extend ``TenantScopedModel``
    and have a nullable ``organization_node`` FK.
    """


__all__ = [
    "ScopedManager",
    "ScopedQuerySet",
    "ScopedSoftDeleteManager",
    "ScopedSoftDeleteQuerySet",
    "TenantWideScopedManager",
    "TenantWideScopedQuerySet",
]
