"""
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:
    from django.contrib.auth.models import 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 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
