"""Core API view mixins — permission checking and audit recording.

Mix these into any ``APIView`` or ``TenantAPIView`` subclass.

``PermissionCheckMixin``
    Adds a ``permission_required`` class attribute.  The IAM engine checks
    that the request actor holds the named permission before the handler runs.

``AuditMixin``
    Records an audit event after every successful write (POST / PATCH /
    PUT / DELETE).  The event action is derived from the HTTP method.

Typical usage::

    from simorgh.core.api.mixins import PermissionCheckMixin, AuditMixin
    from simorgh.core.api.views import TenantListCreateView

    class DocumentList(PermissionCheckMixin, AuditMixin, TenantListCreateView):
        permission_required = "dms.document.list"
        audit_resource_type = "dms.document"

        def get_queryset(self): ...
        def serialize_object(self, obj): ...
        def create_object(self, data): ...
"""
from __future__ import annotations

import logging
from typing import Any

from rest_framework import status
from rest_framework.request import Request
from rest_framework.response import Response

__all__ = [
    "PermissionCheckMixin",
    "AuditMixin",
    "StaffOnlyWriteMixin",
]

logger = logging.getLogger("simorgh.api")


class PermissionCheckMixin:
    """Enforce a named IAM permission before the view handler runs.

    Set ``permission_required`` to a dot-separated codename such as
    ``"dms.document.create"``.  The check is performed by
    :func:`simorgh.apps.iam.engine.is_allowed`.

    For per-method permissions, override :meth:`get_required_permission`::

        def get_required_permission(self, request):
            if request.method == "GET":
                return "dms.document.list"
            return "dms.document.create"

    When the permission check fails the view returns **HTTP 403**.
    """

    permission_required: str | None = None
    # Sentinel used by TenantListCreateView.post() to bypass its own guard
    # when this mixin is responsible for permission checking.
    _uses_permission_check_mixin: bool = True

    def get_required_permission(self, request: Request) -> str | None:
        """Return the codename to check for this request, or ``None``."""
        return self.permission_required  # type: ignore[attr-defined]

    def check_permissions(self, request: Request) -> None:
        """Extend DRF's check_permissions to include the IAM check."""
        super().check_permissions(request)  # type: ignore[misc]

        codename = self.get_required_permission(request)
        if not codename:
            return

        from simorgh.apps.iam.engine import is_allowed
        from simorgh.core.context import current_request_context

        ctx = current_request_context()
        if not is_allowed(ctx, codename):
            from rest_framework.exceptions import PermissionDenied
            raise PermissionDenied(
                detail=f"Permission '{codename}' is required.",
                code="permission_denied",
            )


class AuditMixin:
    """Record an audit event after every successful write operation.

    Class attributes
    ----------------
    audit_resource_type : str
        The ``resource_type`` written to ``AuditLog``.  Defaults to the
        lower-cased ``<app_label>.<model_name>`` string if not set.
    audit_actions_map : dict[str, str]
        Maps HTTP method → audit action string.  Override to customise.

    The mixin hooks into ``finalize_response`` and records the event only
    when the response status code indicates success (2xx) and the method is
    a mutating one (POST/PATCH/PUT/DELETE).
    """

    audit_resource_type: str = ""
    audit_actions_map: dict[str, str] = {
        "POST": "create",
        "PATCH": "update",
        "PUT": "update",
        "DELETE": "delete",
    }

    # Populated by _pre_write for PATCH/DELETE (stores before-state)
    _audit_before: dict | None = None
    _audit_resource_id: str | None = None

    def _get_audit_resource_type(self) -> str:
        if self.audit_resource_type:
            return self.audit_resource_type
        return self.__class__.__name__.lower()

    def _get_audit_resource_id(self, response_data: Any) -> str | None:
        """Extract the resource id from the response data or instance."""
        if self._audit_resource_id:
            return self._audit_resource_id
        if isinstance(response_data, dict):
            return (
                str(response_data.get("id", ""))
                or str(response_data.get("public_id", ""))
                or None
            )
        return None

    def finalize_response(
        self, request: Request, response: Response, *args: Any, **kwargs: Any
    ) -> Response:
        response = super().finalize_response(request, response, *args, **kwargs)  # type: ignore[misc]

        method = request.method.upper() if request.method else ""
        action = self.audit_actions_map.get(method)
        if not action:
            return response

        # Only record on success
        if not (200 <= response.status_code < 300):
            return response

        try:
            from simorgh.core.audit import record_event

            resource_id = self._get_audit_resource_id(
                getattr(response, "data", None)
            )

            # For DELETE we capture the id before the object is gone
            if action == "delete" and self._audit_resource_id:
                resource_id = self._audit_resource_id

            record_event(
                action=f"{self._get_audit_resource_type()}.{action}",
                resource_type=self._get_audit_resource_type(),
                resource_id=resource_id,
                before=self._audit_before,
                after=getattr(response, "data", None) if action != "delete" else None,
            )
        except Exception:
            logger.exception("AuditMixin: failed to record audit event")

        return response


class StaffOnlyWriteMixin:
    """Restrict POST / PATCH / PUT / DELETE to staff users.

    GET requests are allowed for all authenticated users (subject to the
    base class's ``IsAuthenticated`` permission class).

    This is a lightweight alternative to full IAM permission checks for
    admin-managed resources where staff access is sufficient.
    """

    _WRITE_METHODS = frozenset({"POST", "PATCH", "PUT", "DELETE"})

    def check_permissions(self, request: Request) -> None:
        super().check_permissions(request)  # type: ignore[misc]

        method = (request.method or "").upper()
        if method in self._WRITE_METHODS and not getattr(request.user, "is_staff", False):
            from rest_framework.exceptions import PermissionDenied
            raise PermissionDenied(
                detail="Staff access required for write operations.",
                code="staff_required",
            )
