"""DRF integration: tenant resolution, permission classes, error handler.

Rules enforced here (server side, never in the UI):
  * the organization is resolved from X-Organization (or the URL) and *validated* against the
    caller's memberships / platform assignments - the raw id is never trusted;
  * a module that is disabled for the organization rejects every request (403 module_disabled);
  * a view must map the HTTP method to a permission, otherwise the request is denied;
  * unknown organizations and organizations without access are indistinguishable (no enumeration).
"""
from __future__ import annotations

import re

from django.core.cache import cache
from rest_framework.exceptions import NotAuthenticated, PermissionDenied
from rest_framework.permissions import BasePermission
from rest_framework.response import Response
from rest_framework.views import exception_handler as drf_exception_handler

from apps.access.services import AccessContext, accessible_organizations, platform_context, resolve_access
from apps.audit import services as audit
from apps.organizations.models import Organization
from domain.exceptions import DomainError

ORG_HEADER = "HTTP_X_ORGANIZATION"
NO_ACCESS_MESSAGE = "You have no access to this organization."


def _lookup_organization(ref) -> Organization | None:
    ref = str(ref).strip()
    if not ref:
        return None
    if ref.isdigit():
        return Organization.objects.filter(pk=int(ref)).first()
    return Organization.objects.filter(slug=ref).first()


def resolve_tenant(request, org_ref=None) -> AccessContext:
    ref = org_ref if org_ref is not None else request.META.get(ORG_HEADER)
    if not ref:
        raise PermissionDenied("Select an organization (X-Organization header).", code="organization_required")
    organization = _lookup_organization(ref)
    ctx = resolve_access(request.user, organization) if organization else None
    if ctx is None:
        raise PermissionDenied(NO_ACCESS_MESSAGE, code="no_organization_access")
    if ctx.via_platform and cache.add(f"tenant-audit:{request.user.pk}:{organization.pk}", 1, 900):
        audit.record(
            actor=request.user, action="tenant.platform_access", organization=organization,
            entity_type="organization", entity_id=organization.pk, request=request,
        )
    return ctx


def implicit_organization(user) -> Organization | None:
    """The user's only customer organization, if they have exactly one reachable.

    Lets single-organization clients omit X-Organization. It is only a *default*: the result still
    goes through the normal membership/assignment validation. Ambiguity (0 or 2+) yields None.
    """
    customers = [o for o in accessible_organizations(user) if not o.is_platform]
    return customers[0] if len(customers) == 1 else None


def _required_permission(request, view) -> str | None:
    mapping = getattr(view, "permission_map", {})
    return mapping.get(request.method)


class TenantPermission(BasePermission):
    """For views bound to one organization. Configure on the view:

        module_code    = "attendance"            # optional: module that must be enabled
        permission_map = {"GET": "attendance.view", "POST": "attendance.create"}
        org_url_kwarg  = "org_id"                # optional: take the organization from the URL
        allow_implicit_org = True                # optional: single-organization users may omit the header
    """

    def has_permission(self, request, view) -> bool:
        if not (request.user and request.user.is_authenticated):
            raise NotAuthenticated()
        org_ref = view.kwargs.get(getattr(view, "org_url_kwarg", ""), None) if hasattr(view, "kwargs") else None
        if org_ref is None and getattr(view, "allow_implicit_org", False) and not request.META.get(ORG_HEADER):
            fallback = implicit_organization(request.user)
            org_ref = fallback.pk if fallback else None
        ctx = resolve_tenant(request, org_ref)
        request.tenant = ctx
        request.organization = ctx.organization

        module = getattr(view, "module_code", None)
        if module and module not in ctx.enabled_modules:
            raise PermissionDenied("This module is not enabled for the organization.", code="module_disabled")

        permission = _required_permission(request, view)
        min_scope = getattr(view, "scope_map", {}).get(request.method)
        if permission is None or not ctx.has(permission, min_scope):
            raise PermissionDenied("You do not have permission for this operation.", code="permission_denied")
        return True


class PlatformPermission(BasePermission):
    """For NoTeam-wide endpoints (not bound to a customer organization)."""

    def has_permission(self, request, view) -> bool:
        if not (request.user and request.user.is_authenticated):
            raise NotAuthenticated()
        ctx = platform_context(request.user)
        permission = _required_permission(request, view)
        if ctx is None or permission is None or not ctx.has(permission):
            raise PermissionDenied("You do not have permission for this operation.", code="permission_denied")
        request.platform = ctx
        return True


class TenantQuerysetMixin:
    """Limit a queryset to the resolved organization. Use on every tenant-owned model."""

    organization_field = "organization"

    def get_queryset(self):
        return super().get_queryset().filter(**{self.organization_field: self.request.organization})


def _snake(name: str) -> str:
    return re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower().removesuffix("_error")


def api_exception_handler(exc, context):
    if isinstance(exc, DomainError):
        data = {"detail": str(exc), "code": _snake(exc.__class__.__name__)}
        for extra in ("modules", "permissions"):
            if getattr(exc, extra, None):
                data[extra] = getattr(exc, extra)
        return Response(data, status=400)

    response = drf_exception_handler(exc, context)
    if response is not None and isinstance(response.data, dict) and "detail" in response.data:
        code = getattr(response.data["detail"], "code", None)
        if code:
            response.data["code"] = code
    return response
