"""Pure reference for ci-app-access-policy/v1; no storage or transport trust.

Only server-resolved context may reach evaluate(). Never pass a client payload
as context. Required context fields are subject, active, authenticated,
view_role, account_max_role and binding_valid. An expired/missing binding must
never be refreshed implicitly. Optional roles and legacy_roles default to None;
guest_access and legacy_denied default to False; owner_allowed defaults to True.
window_role is an explicit view choice, not a ceiling on the desktop view.
Telegram adapters must supply their independent channel view as view_role.
The evaluator does not mutate either argument. JSON objects are Python dicts.
"""

ROLES = ("guest", "customer", "developer", "admin")
RANK = {role: index for index, role in enumerate(ROLES)}
MAX_REVISION = 9007199254740991
POLICY_FIELDS = frozenset(("schema", "app_id", "revision", "min_role",
                           "max_role", "allowed_users", "user_max_roles"))
CONTEXT_REQUIRED = frozenset(("subject", "active", "authenticated", "view_role",
                              "account_max_role", "binding_valid"))
CONTEXT_FIELDS = CONTEXT_REQUIRED | frozenset(("window_role", "connection_max_role",
    "guest_access", "legacy_roles", "legacy_denied", "owner_allowed"))


def _text(value):
    return (isinstance(value, str) and bool(value.strip())
            and not any(ord(char) < 32 or ord(char) == 127 for char in value))


def _role(value):
    return isinstance(value, str) and value in RANK


def validate_policy(policy):
    """Return bool; reject invalid/unknown fields, never repair a policy."""
    if not isinstance(policy, dict) or set(policy) - POLICY_FIELDS:
        return False
    if policy.get("schema") != "ci-app-access-policy/v1":
        return False
    if not _text(policy.get("app_id")):
        return False
    revision = policy.get("revision")
    if (type(revision) not in (int, float) or not 1 <= revision <= MAX_REVISION
            or revision != int(revision)):
        return False
    for name in ("min_role", "max_role"):
        if name in policy and not _role(policy[name]):
            return False
    if ("min_role" in policy and "max_role" in policy
            and RANK[policy["min_role"]] > RANK[policy["max_role"]]):
        return False
    if "allowed_users" in policy:
        users = policy["allowed_users"]
        if not isinstance(users, list) or not all(_text(user) for user in users):
            return False
        if len(set(users)) != len(users):
            return False
    if "user_max_roles" in policy:
        caps = policy["user_max_roles"]
        if not isinstance(caps, dict):
            return False
        if not all(_text(subject) and _role(role) for subject, role in caps.items()):
            return False
    return True


def _valid_context(context):
    if not isinstance(context, dict):
        return False
    if CONTEXT_REQUIRED - set(context) or set(context) - CONTEXT_FIELDS:
        return False
    for name in ("active", "authenticated", "binding_valid"):
        if type(context[name]) is not bool:
            return False
    for name in ("guest_access", "legacy_denied", "owner_allowed"):
        if name in context and type(context[name]) is not bool:
            return False
    for name in ("view_role", "account_max_role"):
        if not _role(context[name]):
            return False
    for name in ("window_role", "connection_max_role"):
        if context.get(name) is not None and not _role(context[name]):
            return False
    if context.get("legacy_roles") is not None:
        roles = context["legacy_roles"]
        if not isinstance(roles, list) or not all(_role(role) for role in roles):
            return False
        if len(set(roles)) != len(roles):
            return False
    if context["authenticated"]:
        return _text(context["subject"])
    return (context["subject"] is None
            and context["view_role"] == "guest"
            and context["account_max_role"] == "guest"
            and context.get("window_role") in (None, "guest"))


def evaluate(policy, context):
    """Return {allowed, effective_role, reason, policy_revision} fail-closed.

    Missing min/max constraints impose no additional limit. An absent allowlist
    imposes no additional identity condition; [] denies everyone. Subject values
    are exact, case-sensitive identifiers and are never normalized. C0 controls
    and DEL are forbidden in app/subject identifiers. A personal
    cap below an app minimum denies that user, without invalidating the policy.
    A legacy role set stays non-contiguous: clamping never invents missing roles.
    active is the account status: anonymous guests have no account and may use
    active=False. Authenticated inactive accounts never fall back to guest.
    binding_valid must come from the server's current revision/TTL/suspension
    check; this pure function neither authenticates nor revives bindings.
    """
    revision = int(policy["revision"]) if validate_policy(policy) else None

    def deny(reason):
        return {"allowed": False, "effective_role": None, "reason": reason,
                "policy_revision": revision}

    if revision is None:
        return deny("invalid_policy")
    if not _valid_context(context):
        return deny("invalid_context")
    if context["authenticated"] and not context["active"]:
        return deny("inactive_account")
    if not context["binding_valid"]:
        return deny("binding_invalid")
    if not context.get("owner_allowed", True):
        return deny("owner_denied")
    if context.get("legacy_denied", False):
        return deny("legacy_denied")
    if "allowed_users" in policy and context["subject"] not in policy["allowed_users"]:
        return deny("user_not_allowed")

    basis = context.get("window_role") or context["view_role"]
    ceilings = [basis, context["account_max_role"]]
    for role in (policy.get("max_role"), context.get("connection_max_role"),
                 policy.get("user_max_roles", {}).get(context["subject"])):
        if role is not None:
            ceilings.append(role)
    effective = min(ceilings, key=RANK.__getitem__)
    if ((not context["authenticated"] or basis == "guest" or effective == "guest")
            and not context.get("guest_access", False)):
        return deny("guest_denied")
    if context.get("legacy_roles") is not None and effective not in context["legacy_roles"]:
        return deny("role_not_granted")
    if "min_role" in policy and RANK[effective] < RANK[policy["min_role"]]:
        return deny("minimum_not_met")
    return {"allowed": True, "effective_role": effective, "reason": "allowed",
            "policy_revision": revision}
