"""Lightweight redaction helpers for ledger / log writes.

Used by ``utils.task_ledger`` (and any future caller) to strip plausible
secrets from strings before they hit disk. Conservative by design: prefer
false positives over leaking a token. Patterns target the most common
formats (Bearer/Basic/sha-256-style HMACs, API-key prefixes, password=
keyword).

Public surface:

    redact_text(text)            -> str
    redact_value(value)          -> Any  (recursive on dict/list)
    enforce_no_control_chars(s)  -> raises ValueError when invalid
    cap_str(value, max_len)      -> str
"""

from __future__ import annotations

import re
from typing import Any

_REDACTED = "[REDACTED]"

# Conservative high-signal patterns. Order matters; longer matches first.
_PATTERNS: list[tuple[re.Pattern[str], str]] = [
    (re.compile(r"\b(?:Bearer|Basic)\s+[A-Za-z0-9._\-+/=]{8,}", re.IGNORECASE), "Bearer " + _REDACTED),
    (re.compile(r"\bsk-[A-Za-z0-9]{16,}"), _REDACTED),  # OpenAI-style
    (re.compile(r"\bxox[abprs]-[A-Za-z0-9-]{10,}"), _REDACTED),  # Slack
    (re.compile(r"\bgh[pousr]_[A-Za-z0-9]{16,}"), _REDACTED),  # GitHub PAT
    (re.compile(r"\bAKIA[0-9A-Z]{12,20}"), _REDACTED),  # AWS access key id
    # password=, secret=, token=, api_key= keyword + value (until whitespace, comma, or ")
    (re.compile(
        r"((?:password|secret|token|api[_-]?key|hmac|signature)\s*[:=]\s*)(['\"]?)([^\s,'\"\\]{6,})\2",
        re.IGNORECASE,
    ), r"\1\2" + _REDACTED + r"\2"),
    # x-mugi-webhook-token header values
    (re.compile(r"(X-Mugi-(?:Webhook-Token|Signature)\s*[:=]\s*)(\S{6,})", re.IGNORECASE), r"\1" + _REDACTED),
    # Generic 32+ char hex blobs that smell like sha256 hashes / hmac sigs.
    (re.compile(r"\b[a-f0-9]{40,128}\b", re.IGNORECASE), _REDACTED),
]


def redact_text(text: Any) -> str:
    """Apply secret patterns and return a redacted string copy."""
    if text is None:
        return ""
    out = str(text)
    for pat, repl in _PATTERNS:
        out = pat.sub(repl, out)
    return out


def redact_value(value: Any) -> Any:
    """Recursively redact strings inside dicts/lists; pass other types through."""
    if isinstance(value, str):
        return redact_text(value)
    if isinstance(value, dict):
        return {str(k): redact_value(v) for k, v in value.items()}
    if isinstance(value, (list, tuple)):
        out = [redact_value(v) for v in value]
        return out if isinstance(value, list) else tuple(out)
    return value


def enforce_no_control_chars(text: str) -> None:
    """Raise ValueError if text contains control chars outside \\t and \\n."""
    for ch in text:
        if (ord(ch) < 0x20) and ch not in ("\t", "\n"):
            raise ValueError(f"control character 0x{ord(ch):02x} not allowed")


def cap_str(value: Any, max_len: int) -> str:
    """Coerce to str, redact, strip control chars by replacement, and cap length."""
    raw = redact_text(value)
    cleaned = "".join(c if (c.isprintable() or c in ("\t", "\n")) else " " for c in raw)
    if max_len <= 0:
        return ""
    if len(cleaned) <= max_len:
        return cleaned
    return cleaned[: max_len - 1] + "…"
