"""
Permission codename constants for the Discord audit module.

All codenames are namespaced to the ``aa_discord_audit`` Django app
label, which equals the importable Python package name.

Operators see these codenames in Django admin's permission matrix
and can grant them independently per the threat model in
§"Permissions" — the four ``manage_*`` codenames in particular are
split per blast radius so a junior with ``manage_bot_account_uid``
cannot also defang the audit by editing ``ManagedRoleException``.

This module is the single source of truth for the app label; every
other site that needs to assemble a ``"<app_label>.<codename>"``
string must import the constants from here rather than hardcoding.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Final

from django.db.models import Q

if TYPE_CHECKING:  # pragma: no cover
    from django.contrib.auth.models import AbstractUser, Permission

APP_LABEL: Final[str] = "aa_discord_audit"

RUN_AUDIT: Final[str] = f"{APP_LABEL}.run_audit"
ACKNOWLEDGE_INITIAL_AUDIT: Final[str] = (
    f"{APP_LABEL}.acknowledge_initial_audit"
)
MANAGE_DISCORD_IDENTITY: Final[str] = f"{APP_LABEL}.manage_discord_identity"
MANAGE_ROLE_EXCEPTION: Final[str] = f"{APP_LABEL}.manage_role_exception"
MANAGE_PROTECTED_MEMBER: Final[str] = f"{APP_LABEL}.manage_protected_member"
MANAGE_BOT_ACCOUNT_UID: Final[str] = f"{APP_LABEL}.manage_bot_account_uid"
MANAGE_FINDING_OVERRIDE: Final[str] = f"{APP_LABEL}.manage_finding_override"

# Django auto-generated view permissions; admin + web UI gating
# consult these.
VIEW_AUDITRUN: Final[str] = f"{APP_LABEL}.view_auditrun"
VIEW_AUDITFINDING: Final[str] = f"{APP_LABEL}.view_auditfinding"
VIEW_AUDITINVOCATION: Final[str] = f"{APP_LABEL}.view_auditinvocation"
VIEW_CONFIGCHANGELOG: Final[str] = f"{APP_LABEL}.view_configchangelog"


def can_acknowledge_initial(user: AbstractUser | None) -> bool:
    """
    Return whether ``user`` may release the first-run safety lock.

    AND-gate: releasing the lock requires BOTH ``run_audit`` AND
    ``acknowledge_initial_audit``. The single source of truth shared by
    the admin (``InitialAuditAcknowledgementAdmin.has_add_permission``)
    and the ``audit_acknowledge_initial`` CLI command, so the two paths
    can never disagree about who may release the lock.

    The ``None``-guard is mandatory: the CLI actor resolver returns
    ``AbstractUser | None``, and calling ``.has_perm`` on ``None`` would
    raise ``AttributeError`` on an unresolved actor.
    """
    return (
        user is not None
        and user.has_perm(RUN_AUDIT)
        and user.has_perm(ACKNOWLEDGE_INITIAL_AUDIT)
    )


def request_user_has_perm(request: object, perm: str) -> bool:
    """Convenience wrapper over ``request.user.has_perm(perm)``."""
    # ``request.user`` is typed as ``AbstractBaseUser | AnonymousUser``
    # by django-stubs; ``has_perm`` exists on every user class AA
    # actually wires up (``AnonymousUser`` always returns ``False``,
    # configured user models inherit ``PermissionsMixin``). One helper
    # centralises the ``request.user`` narrowing via ``getattr`` +
    # ``bool`` that the views, admin, and auth-hook layers would
    # otherwise each repeat in their own copies.
    user = getattr(request, "user", None)
    if user is None:
        return False
    return bool(user.has_perm(perm))


def user_has_perm_q(perm: Permission, *, active_only: bool = False) -> Q:
    """
    Return a Django Q expression matching users who hold ``perm``.

    Captures the three places Django grants a permission:
    1. ``user_permissions`` (a direct grant on the User row).
    2. ``groups__permissions`` (inherited via group membership).
    3. ``is_superuser=True`` (the explicit override).

    The formula appears in two places in the audit and historically
    drifted between them — the policy classifier passes
    ``active_only=True`` because deactivated users must classify as
    LINKED_NO_PERM (deactivation is the canonical off-boarding
    revocation), while the AA-notify fan-out passes the default
    ``active_only=False`` so a staff account deactivated *after*
    receiving an audit assignment still gets the operator-visible
    summary line. Routing the divergence through one explicit flag
    makes the trade-off auditable instead of buried in two slightly
    different ``filter`` calls.
    """
    base = (
        Q(user_permissions=perm)
        | Q(groups__permissions=perm)
        | Q(is_superuser=True)
    )
    return base & Q(is_active=True) if active_only else base
