"""Shared credential-rejection guard for web UI registries (#427, #798).

Registries are operator-editable declarative files that the web UI loads and,
for the worker registry, writes back. None of them may ever carry a secret:
credentials belong in the keychain and reach worker processes through
environment injection, never through a file the browser layer can read.

The check is structural rather than value-based on purpose. A value scanner has
to guess what a secret looks like; a key scanner refuses the *shape* of a
credential field, so an operator cannot introduce one by accident and a later
loader cannot silently pass one through.
"""

from __future__ import annotations

from typing import Any

_FORBIDDEN_EXACT_KEYS = frozenset({
    "token",
    "password",
    "secret",
    "credential",
    "auth",
    "api_key",
    "api-key",
})
_FORBIDDEN_KEY_PREFIXES = ("auth_", "api_key_", "api-key_")
_FORBIDDEN_KEY_SUFFIXES = ("_token", "_secret", "_password", "_credential", "_auth")


def is_forbidden_key(key: str) -> bool:
    """Return True when *key* names a credential field."""
    lowered = key.lower()
    if lowered in _FORBIDDEN_EXACT_KEYS:
        return True
    return (
        lowered.startswith(_FORBIDDEN_KEY_PREFIXES)
        or lowered.endswith(_FORBIDDEN_KEY_SUFFIXES)
    )


def reject_credential_keys(obj: Any, *, path: str = "", subject: str = "registry") -> None:
    """Raise ValueError when *obj* carries a credential-shaped key at any depth."""
    if isinstance(obj, dict):
        for key, value in obj.items():
            key_path = f"{path}.{key}" if path else key
            if is_forbidden_key(key):
                raise ValueError(f"{subject} must not store credentials ({key_path})")
            reject_credential_keys(value, path=key_path, subject=subject)
    elif isinstance(obj, list):
        for index, item in enumerate(obj):
            reject_credential_keys(item, path=f"{path}[{index}]", subject=subject)
