"""Turn an upstream exception into something safe to store — the CLI's copy.

Kept behaviourally identical to api/src/abrade_api/services/
upstream_errors.py, and guarded by a parity test for the same reason the
canary module is: the offline path is the half nobody is watching, and it
handles the *same* attacker-controlled input.

The hosted API was fixed for this and the CLI was not, which left the
worse of the two exposed. `abrade run` stored `str(exc)` verbatim, so an
agent-under-test endpoint could reply

    401 {"error": {"message": "invalid key: received sk-ant-..."}}

and the CLI would write that credential into .abrade/runs/<id>.json on
the customer's disk — and then upload it on `abrade push`. The endpoint is
customer-supplied and sees the Authorization header, so producing that
body requires nothing clever.

"""

from __future__ import annotations

_REDACTED = "[redacted]"

# Below this, a "secret" is too short to redact without mangling ordinary
# text. Real credentials are far longer; this only guards against a
# pathological caller.
_MIN_REDACTABLE = 8


def redact(text: str, *secrets: str | None) -> str:
    """Remove known secret values from a string."""
    for secret in secrets:
        if secret and len(secret) >= _MIN_REDACTABLE:
            text = text.replace(secret, _REDACTED)
    return text


def contains_secret(text: str, *secrets: str | None) -> bool:
    """Whether any known live secret appears verbatim in `text`.

    Separate from `redact` because the two answer different questions and
    the caller needs both: redaction makes a string safe to keep, this
    records that there was something to redact. Losing that turns a
    credential disclosure into a silently cleaned-up reply.

    Same length floor as `redact`, so the two can never disagree about
    what counts as a secret.
    """
    return any(
        secret and len(secret) >= _MIN_REDACTABLE and secret in text
        for secret in secrets
    )


def describe(exc: Exception, *, secrets: tuple[str | None, ...] = ()) -> str:
    """A short, storable description of an upstream failure.

    Deliberately does not include `str(exc)`. For an HTTP error that
    string contains the response body, which the other side controls.
    """
    parts = [type(exc).__name__]

    # The SDK sets `status_code` on HTTP errors. It comes from the status
    # line, not the body, so it is safe.
    status = getattr(exc, "status_code", None)
    if isinstance(status, int):
        parts.append(f"HTTP {status}")

    return redact(" · ".join(parts), *secrets)
