"""
Allowlist-based ``cli_argv`` redaction.

Anything not on the list records as ``[REDACTED]`` regardless of
shape — closes the collateral-leakage gap where a wrapper script
passing ``OAUTH_CLIENT_SECRET=...`` to the management command
would have slipped past a regex matcher.
"""

from collections.abc import Sequence
from typing import Final

# Sentinel emitted in place of any argv token that fails the allowlist.
_REDACTED: Final[str] = "[REDACTED]"

# Known argument keys that take a value as the next token. The value
# is preserved alongside the key.
#
# Read-only diagnostic flags (``--explain``, ``--diff``,
# ``--from-fixture``, ``--policy-preview``) are listed here even
# though their subcommands route through
# ``_begin_readonly_cli_invocation`` — which today does NOT write
# an ``AuditInvocation`` row. Pre-listing them removes the implicit
# coupling between argv_redact and the readonly subcommands' write
# semantics: a future maintainer who promotes any of them to record
# an invocation row (for forensics-trail completeness) gets a
# meaningful cli_argv automatically, instead of silent
# ``["[REDACTED]", "[REDACTED]"]``.
_ALLOWED_VALUE_FOLLOWS: Final[frozenset[str]] = frozenset(
    {
        "--action",
        "--limit",
        "--user-id",
        "--resume",
        "--abandon",
        "--explain",
        "--diff",
        "--from-fixture",
        "--policy-preview",
    }
)

# Boolean / flag keys (no value follows).
_ALLOWED_FLAGS: Final[frozenset[str]] = frozenset(
    {
        "--no-notify",
        "--yes",
        "--json",
    }
)

# Full allowlist derived from the two role-segregated sets so the
# value-vs-flag classification cannot drift from the membership check.
_ALLOWED_ARGV_KEYS: Final[frozenset[str]] = (
    _ALLOWED_VALUE_FOLLOWS | _ALLOWED_FLAGS
)


def redact_argv(argv: Sequence[str]) -> list[str]:
    """
    Return a copy of ``argv`` with non-allowlisted tokens redacted.

    Handles both ``--key value`` (separate tokens) and ``--key=value``
    (single token, argparse-accepted) forms. Allowlisted keys without
    a following value are emitted verbatim. Trailing allowlisted
    value-bearing keys with no following value are also emitted
    verbatim (no index error, no fabricated value).
    """
    out: list[str] = []
    i = 0
    n = len(argv)
    while i < n:
        tok = argv[i]
        # ``--key=value`` form: pre-split on '=' so the membership
        # check sees the bare key.
        if tok.startswith("--") and "=" in tok:
            key, _, value = tok.partition("=")
            if key in _ALLOWED_ARGV_KEYS:
                out.append(key)
                if key in _ALLOWED_VALUE_FOLLOWS:
                    out.append(value)
                else:
                    out.append(_REDACTED)
            else:
                out.append(_REDACTED)
            i += 1
            continue
        if tok in _ALLOWED_ARGV_KEYS:
            out.append(tok)
            if (
                tok in _ALLOWED_VALUE_FOLLOWS
                and i + 1 < n
                and not argv[i + 1].startswith("-")
            ):
                out.append(argv[i + 1])
                i += 2
                continue
        else:
            out.append(_REDACTED)
        i += 1
    return out
