"""Nech.PL APIs — the scope convention, in one place.

Scopes are hierarchical, ':'-delimited paths:  realm:domain:seg:seg…

  realm   the capability class. `api` = the HTTP APIs; `admin` = the control
          plane (tenant/token management); future siblings: `mail`, `data`, …
  domain  the sub-API (for the `api` realm): audio, geo, …
  seg…    the route path under that API, version stripped.

A route's REQUIRED scope is derived from its public path, so access control
tracks the URL by construction — add `/audio/foo/bar` and it simply requires
`api:audio:foo:bar`, no table to maintain:

    /audio/v1/analyze/emotion  ->  api:audio:analyze:emotion
    /audio/v1/grade            ->  api:audio:grade
    /geo/v1/reverse            ->  api:geo:reverse

A GRANTED scope matches a required one if it is the same, or an ancestor ending
in `*` (a subtree grant):

    *                          everything (break-glass superadmin)
    api:*                      every HTTP API (present & future)
    api:audio:*                all of the audio API
    api:audio:analyze:*        the analyze subtree
    api:audio:analyze:emotion  that one route, exactly
    admin:*                    the control plane (NOT granted by `api:*` or, by
                               convention, by a bare domain grant)

Note the deliberate asymmetry: a NON-wildcard grant is exact — `api:audio` does
NOT grant `api:audio:grade`. Only the `*`-suffixed form opens a subtree. This
keeps "give them exactly this route" safe and "give them everything under here"
explicit.

Pure stdlib, no deps — the same file is vendored verbatim into each service so
the gateway and every API agree on the rule. Keep it dependency-free and in
sync (services drift-guard against this copy).
"""
from __future__ import annotations

REALMS = ("api", "admin")  # extensible: add "mail", "data", … as they appear


def required_for(domain: str, route: str, realm: str = "api") -> str:
    """The scope a request needs, from its (domain, route). `route` is the
    path under the API with the version already stripped (e.g. "/analyze/emotion"
    or "/"). Returns e.g. "api:audio:analyze:emotion"; "api:audio" for the root."""
    segs = [s for s in route.strip("/").split("/") if s]
    parts = [realm]
    if domain:
        parts.append(domain)
    parts.extend(segs)
    return ":".join(parts)


def matches(granted: str, required: str) -> bool:
    """True if a single GRANTED scope authorizes a REQUIRED scope.

    Wildcard `*` (only ever the last segment) opens the whole subtree at that
    level; otherwise the grant must equal the requirement exactly."""
    g = granted.split(":")
    r = required.split(":")
    for i, seg in enumerate(g):
        if seg == "*":
            return True                 # subtree grant — covers this node & below
        if i >= len(r) or seg != r[i]:
            return False
    return len(g) == len(r)             # exhausted without '*' → must be exact


def match_any(granted, required: str) -> bool:
    """True if ANY of the caller's granted scopes authorizes `required`."""
    return any(matches(g, required) for g in granted)


def validate(scope: str) -> bool:
    """A scope is well-formed if every segment is non-empty and `*` appears only
    as the final segment (a `*` mid-path like `api:*:emotion` is meaningless)."""
    if not scope:
        return False
    parts = scope.split(":")
    for i, p in enumerate(parts):
        if p == "":
            return False
        if p == "*" and i != len(parts) - 1:
            return False
    return True
