"""Business authorization only; Django's native has_perm/backend are unchanged."""
import re

from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.core.exceptions import PermissionDenied
from django.db.models import Count, Exists, F, Q, QuerySet

from apps.organization.assignments import UserOrganizationAssignment
from apps.organization.models import Company, ServiceCenter
from .scopes import ADAPTERS, assignment_is_valid, assignment_reaches

PERMISSION_PATTERN = re.compile(r"[a-zA-Z_][a-zA-Z0-9_]*\.[a-zA-Z_][a-zA-Z0-9_]*", re.ASCII)

# The scope models a capability question is answered against. A capability holds
# when the actor has an authorized row of either kind, except a company-only
# capability, which is answered by a company row alone.
CAPABILITY_MODELS = (Company, ServiceCenter)


def _saved_instance(instance, model):
    return (isinstance(instance, model) and instance.pk is not None
            and not instance._state.adding and instance._state.db == "default")


def _authenticated_user(user):
    return _saved_instance(user, get_user_model()) and bool(user.is_authenticated)


def authorized_queryset(*, user, permission, queryset):
    """Lazily filter a supported model queryset using one SQL authorization predicate.

    Permission is an independent capability identifier; its content type need not
    equal the scope object's model (e.g. view_servicecenter against a Company scope).
    """
    if not isinstance(queryset, QuerySet):
        raise TypeError("queryset must be a Django QuerySet")
    adapter = ADAPTERS.get(queryset.model)
    if (adapter is None or not _authenticated_user(user)
            or not isinstance(permission, str) or PERMISSION_PATTERN.fullmatch(permission) is None
            or queryset.db != "default" or queryset.query.is_sliced or queryset.query.combinator):
        return queryset.model._default_manager.none()
    app_label, codename = permission.split(".")
    # Deny absent or ambiguous app_label.codename identifiers, even for superusers.
    known_permission = Permission.objects.filter(content_type__app_label=app_label, codename=codename).values(
        "content_type__app_label", "codename",
    ).annotate(matches=Count("pk")).filter(matches=1)
    superusers = get_user_model().objects.filter(pk=user.pk, is_active=True, is_superuser=True)
    paths = UserOrganizationAssignment.objects.filter(assignment_is_valid()).filter(
        user_id=user.pk,
        role_assignments__user_id=F("user_id"),
        role_assignments__is_active=True,
        role_assignments__role__is_active=True,
        role_assignments__role__permissions__content_type__app_label=app_label,
        role_assignments__role__permissions__codename=codename,
    ).filter(adapter.containment)
    return queryset.filter(
        Exists(known_permission),
        Q(Exists(superusers)) | (adapter.valid_target & Q(Exists(paths))),
    )


def is_authorized(*, user, permission, target):
    """Default-deny boolean check for a saved, supported organizational target."""
    model = type(target)
    if model not in ADAPTERS or not _saved_instance(target, model):
        return False
    return authorized_queryset(user=user, permission=permission,
                               queryset=model._default_manager.filter(pk=target.pk)).exists()


def require_permission(*, user, permission, target):
    """Minimal integration for server-side views/services; errors reveal no scope data."""
    if not is_authorized(user=user, permission=permission, target=target):
        raise PermissionDenied("Permission denied.")


def _supported_permission(permission):
    return isinstance(permission, str) and PERMISSION_PATTERN.fullmatch(permission) is not None


def _known_permissions(app_labels, codenames):
    """Permission keys that resolve to exactly one Django permission row."""
    return {
        (row["content_type__app_label"], row["codename"]) for row in
        Permission.objects.filter(content_type__app_label__in=app_labels, codename__in=codenames)
        .values("content_type__app_label", "codename").annotate(matches=Count("pk")).filter(matches=1)
    }


def _granted_permission_keys_reaching(user, model, app_labels, codenames):
    """Permission keys this user holds through an assignment that reaches `model`.

    Mirrors the `paths` construction in :func:`authorized_queryset` clause for
    clause; the only difference is that the permission is a row rather than a
    parameter, so many permissions resolve in one statement.
    """
    return set(
        UserOrganizationAssignment.objects.filter(assignment_is_valid()).filter(
            user_id=user.pk,
            role_assignments__user_id=F("user_id"),
            role_assignments__is_active=True,
            role_assignments__role__is_active=True,
            role_assignments__role__permissions__content_type__app_label__in=app_labels,
            role_assignments__role__permissions__codename__in=codenames,
        ).filter(assignment_reaches(model)).values_list(
            "role_assignments__role__permissions__content_type__app_label",
            "role_assignments__role__permissions__codename",
        ).distinct())


def _scope_satisfaction(user, model, superuser, app_labels, codenames):
    """Permission keys with an authorized `model` row, or None meaning "every known key".

    None is returned for an active superuser whenever an authorized row of that
    model exists, mirroring `Q(Exists(superusers)) | (valid_target & Exists(paths))`.
    """
    if superuser:
        return None if model._default_manager.filter(ADAPTERS[model].valid_target).exists() else frozenset()
    return frozenset(_granted_permission_keys_reaching(user, model, app_labels, codenames))


def authorized_capability_map(*, user, permissions):
    """Both scope modes for many permissions, resolved in one bounded pass.

    Returns ``{(permission, company_only): bool}``. This is the authoritative
    bulk form of the same question :func:`authorized_capabilities` answers, and
    it exists so a caller that needs both a company-only and a general capability
    does not pay for two separate resolutions of the same permission set.
    """
    results = authorized_capabilities(user=user, permissions=permissions, company_only=None)
    return {(permission, company_only): allowed
            for (permission, company_only), allowed in results.items()}


def authorized_capabilities(*, user, permissions, company_only=False):
    """Bulk equivalent of ``authorized_queryset(...).exists()`` over many permissions.

    Returns ``{permission: bool}`` for every requested permission, or
    ``{(permission, company_only): bool}`` when ``company_only`` is ``None``.
    A capability holds when the permission resolves to exactly one Django
    permission, and either the actor is an active superuser with an authorized
    target of that kind, or a valid active assignment of theirs granting the
    permission reaches an authorized target of that kind. A company-only
    capability is answered by a company target alone; otherwise a company or a
    service-center target answers it. This is the same question
    ``apps.operations.queries.capable`` asks one permission at a time.

    The query count is bounded and does not grow with the number of permissions
    requested, which is why the navigation shell resolves all of its questions in
    one call instead of one call per link. There is no cache: every call re-reads
    current state.
    """
    both_modes = company_only is None
    requested = []
    for permission in permissions:
        if permission not in requested:
            requested.append(permission)
    denied = {(permission, mode): False for permission in requested for mode in (False, True)} if both_modes \
        else {permission: False for permission in requested}
    if not requested or not _authenticated_user(user):
        return denied
    usable = [permission for permission in requested if _supported_permission(permission)]
    if not usable:
        return denied
    app_labels = sorted({permission.split(".")[0] for permission in usable})
    codenames = sorted({permission.split(".")[1] for permission in usable})
    known = _known_permissions(app_labels, codenames)
    superuser = get_user_model().objects.filter(
        pk=user.pk, is_active=True, is_superuser=True).exists()
    models_needed = (CAPABILITY_MODELS[0],) if company_only else CAPABILITY_MODELS
    satisfaction = {model: _scope_satisfaction(user, model, superuser, app_labels, codenames)
                    for model in models_needed}
    if not both_modes:
        results = {}
        for permission in requested:
            key = tuple(permission.split(".")) if _supported_permission(permission) else None
            results[permission] = key in known and any(
                satisfied is None or key in satisfied for satisfied in satisfaction.values())
        return results
    # Company-only is answered by a company target; the general mode also accepts
    # a service-center target.
    company = satisfaction[CAPABILITY_MODELS[0]]
    center = satisfaction[CAPABILITY_MODELS[1]]
    results = {}
    for permission in requested:
        key = tuple(permission.split(".")) if _supported_permission(permission) else None
        holds_company = key in known and (company is None or key in company)
        holds_center = key in known and (center is None or key in center)
        results[(permission, True)] = holds_company
        results[(permission, False)] = holds_company or holds_center
    return results


def scope_contains(*, assignment, target):
    """Check active scope containment only. This is not a permission decision."""
    model = type(target)
    adapter = ADAPTERS.get(model)
    if (adapter is None or not _saved_instance(target, model)
            or not _saved_instance(assignment, UserOrganizationAssignment)):
        return False
    sources = UserOrganizationAssignment.objects.filter(pk=assignment.pk).filter(
        assignment_is_valid(), adapter.containment,
    )
    return model._default_manager.filter(pk=target.pk).filter(adapter.valid_target, Exists(sources)).exists()
