"""Assert every UserRole has either a Django Group permission set OR an access mixin reference.

Run via ``python manage.py audit_roles``. Prints a 3-column matrix and exits
non-zero if any role lacks both a seeded Group permission and any reference
in a known role-gate file. Used as a regression gate via
``apps/core/permissions/tests/test_role_coverage.py``.
"""
from __future__ import annotations

import re
from pathlib import Path

from django.contrib.auth.models import Group
from django.core.management.base import BaseCommand

from apps.core.permissions.roles import UserRole

# Files we accept as evidence that a role is referenced by an access gate.
# Globs are resolved relative to the project root (BASE_DIR).
ROLE_GATE_GLOBS: tuple[str, ...] = (
    "apps/**/role_gates.py",
    "apps/**/permissions/mixins.py",
    "apps/**/permissions/policies.py",
    "apps/**/permissions/access.py",
    "apps/**/mixins.py",
    "apps/**/views.py",
    "apps/**/funding_views.py",
    "apps/**/admin_views.py",
    "apps/**/views_access.py",
    "apps/**/dashboard/role_groups.py",
    "apps/**/dashboard/widgets/*.py",
    "apps/**/management/commands/seed_roles.py",
    "apps/**/forms.py",
    "apps/**/forms_user_access.py",
)


def _project_root() -> Path:
    # apps/core/management/commands/audit_roles.py → project root is parents[4]
    return Path(__file__).resolve().parents[4]


def _collect_role_refs() -> dict[str, set[str]]:
    """Return ``{role_value: {file_paths_referencing_role}}`` from gate files."""
    root = _project_root()
    refs: dict[str, set[str]] = {r.value: set() for r in UserRole}
    role_values = list(refs.keys())
    # Match either the literal string "role_value" OR UserRole.NAME constants.
    string_re = re.compile(r"['\"](" + "|".join(re.escape(v) for v in role_values) + r")['\"]")
    constant_re = re.compile(r"UserRole\.([A-Z_]+)")

    for pattern in ROLE_GATE_GLOBS:
        for path in root.glob(pattern):
            if not path.is_file():
                continue
            try:
                text = path.read_text(encoding="utf-8", errors="ignore")
            except (OSError, UnicodeDecodeError):
                continue
            rel = str(path.relative_to(root))
            for match in string_re.finditer(text):
                refs[match.group(1)].add(rel)
            for match in constant_re.finditer(text):
                attr = match.group(1)
                try:
                    role_value = UserRole[attr].value
                except KeyError:
                    continue
                refs[role_value].add(rel)
    return refs


def _group_perm_counts() -> dict[str, int]:
    """Return ``{role_value: count_of_permissions_on_matching_group}``."""
    role_values = [r.value for r in UserRole]
    counts: dict[str, int] = {v: 0 for v in role_values}
    for group in Group.objects.filter(name__in=role_values).prefetch_related("permissions"):
        counts[group.name] = group.permissions.count()
    return counts


def audit() -> tuple[list[tuple[str, int, int]], list[str]]:
    """Compute the audit matrix.

    Returns ``(rows, uncovered)`` where each row is ``(role, group_perms, mixin_refs)``
    and ``uncovered`` is the list of role values that have BOTH 0 group perms AND
    0 mixin refs.
    """
    refs = _collect_role_refs()
    perms = _group_perm_counts()
    rows: list[tuple[str, int, int]] = []
    uncovered: list[str] = []
    for role in UserRole:
        rv = role.value
        n_perms = perms.get(rv, 0)
        n_refs = len(refs.get(rv, set()))
        rows.append((rv, n_perms, n_refs))
        if n_perms == 0 and n_refs == 0:
            uncovered.append(rv)
    return rows, uncovered


class Command(BaseCommand):
    help = "Assert every UserRole has either Group permissions or a role-gate reference."

    def handle(self, *args, **options):
        rows, uncovered = audit()
        col1 = max(len(r[0]) for r in rows)
        header = f"{'role'.ljust(col1)}  group_perms  mixin_refs"
        self.stdout.write(header)
        self.stdout.write("-" * len(header))
        for rv, n_perms, n_refs in rows:
            line = f"{rv.ljust(col1)}  {n_perms:>11}  {n_refs:>10}"
            if n_perms == 0 and n_refs == 0:
                line = self.style.ERROR(line + "   <- UNCOVERED")
            self.stdout.write(line)

        self.stdout.write("")
        if uncovered:
            self.stdout.write(
                self.style.ERROR(
                    f"FAIL: {len(uncovered)} role(s) without any gate: {', '.join(uncovered)}",
                ),
            )
            self.stdout.write(
                "Add a Django Group permission via apps/core/management/commands/seed_roles.py "
                "OR reference the role in a role-gate file.",
            )
            raise SystemExit(1)
        self.stdout.write(
            self.style.SUCCESS(f"OK: all {len(rows)} roles covered."),
        )
