"""Shared Action Point plumbing for the role dashboards.

An "action point" is one pending-work item on a dashboard: a label, a count,
the exact record ids behind that count, and the existing module route that can
act on it. Recruiter, Project Manager and Hiring Manager dashboards all build
their points with `make_point()` and hand them to `filter_action_points()`.

RBAC
----
Points are filtered SERVER-SIDE against the logged-in user's effective
permissions, taken from the project's existing Role -> Group -> Permission
chain via `User.get_all_permissions()` — the same source `MenuTreeView`,
`DjangoModelPermissionsWithView` and the frontend `useRequirePermission` guard
already use. Admins (role ADMIN / superuser) hold everything and pass.

The permission a point requires is the VIEW permission of the module it opens,
resolved from the `menus` table — the project's existing route -> permission
map, maintained by admins on the Menus screen — and falling back to the
module's standard Django view permission only when a route has no menu row
carrying one. No role name is hardcoded anywhere: grant
`candidates.view_candidate` to a group and every candidate-facing action point
appears for it, revoke it and they all disappear.

A point the user may not view is dropped from the response entirely, so its
count never reaches the browser and a direct API call cannot reveal it either.
The surviving points carry their resolved `permissions` list so the UI can
apply the same check when deciding whether a card is clickable.
"""

from apps.menus.models import Menu

# Route -> view permission, used ONLY when a route has no active menu row
# carrying a `view_*` permission_code. These are the standard Django model
# permissions the matching list pages are already protected with.
FALLBACK_VIEW_PERMISSION = {
    "/jobs": "jobs.view_jobdescription",
    "/candidates": "candidates.view_candidate",
    "/clients": "clients.view_client",
    "/reports": "reports.view_reports",
}


def _base_route(route):
    """Normalise a route for menu lookup: no query string, no trailing slash."""
    base = (route or "").split("?")[0].split("#")[0].rstrip("/")
    return base or "/"


def _menu_view_permissions():
    """route -> {view permission codes}, read from the active sidebar menus.

    Only `view_*` codes gate READ access to a page; `add_*` / `change_*` /
    `publish_*` codes sitting on the same route unlock actions inside it, not
    the page itself, so they are ignored here.
    """
    mapping = {}
    rows = (
        Menu.objects.filter(is_active=True)
        .exclude(route="")
        .exclude(permission_code="")
        .values_list("route", "permission_code")
    )
    for route, code in rows:
        if ".view_" not in code:
            continue
        mapping.setdefault(_base_route(route), set()).add(code)
    return mapping


def required_permissions(route, menu_map=None):
    """The permission codes that unlock `route` — holding ANY one is enough.

    Mirrors the sidebar: a route reachable through several menu entries is open
    to anyone who can see one of them. An empty result means the route is
    ungated (same as a menu row with a blank permission_code).
    """
    if menu_map is None:
        menu_map = _menu_view_permissions()
    base = _base_route(route)
    codes = set(menu_map.get(base) or ())
    if not codes:
        fallback = FALLBACK_VIEW_PERMISSION.get(base)
        if fallback:
            codes = {fallback}
    return sorted(codes)


def _is_admin(user):
    return bool(user and (getattr(user, "role", None) == "ADMIN" or user.is_superuser))


def can_view_route(user, route, menu_map=None):
    """Whether `user` may view the module behind `route`.

    The same rule `filter_action_points` applies to a single point — useful for
    gating a whole dashboard widget that reports on one module's records.
    """
    if _is_admin(user):
        return True
    codes = required_permissions(route, menu_map)
    if not codes:
        return True  # ungated route (mirrors a blank menu permission_code)
    return bool(set(codes) & set(user.get_all_permissions()))


def make_point(key, label, ids, route, icon, tone, urgent=False, description=""):
    """Build one action point. `ids` are the exact records behind the count, so
    the destination list can show precisely those (count == len(ids))."""
    ids = list(ids)
    point = {
        "key": key,
        "label": label,
        "count": len(ids),
        "ids": ids,
        "route": route,
        "icon": icon,
        "tone": tone,
    }
    if description:
        point["description"] = description
    if urgent:
        point["urgent"] = True
    return point


def filter_action_points(user, points):
    """Drop every point whose target module the user has no View permission for.

    Returns the surviving points, each annotated with the `permissions` list
    that unlocked it (any one of them is sufficient).
    """
    menu_map = _menu_view_permissions()
    held = None if _is_admin(user) else set(user.get_all_permissions())

    visible = []
    for point in points:
        codes = required_permissions(point.get("route", ""), menu_map)
        if held is not None and codes and not (set(codes) & held):
            continue
        visible.append({**point, "permissions": codes})
    return visible
