"""
``X-Audit-Log-Reason`` builders for Discord API calls.

Discord caps the audit-log reason header at 512 bytes. The audit
module sanitizes Cc (control) and Cf (format) Unicode categories
before transmission, both because Discord's audit-log UI renders
some control / bidi characters in surprising ways and because
operator-controlled segments would otherwise be a log-injection
surface.
"""

import unicodedata
from typing import Final

from .permissions import APP_LABEL

# Discord-side hard limit on the X-Audit-Log-Reason header.
_MAX_REASON_BYTES: Final[int] = 512

# Cap on operator-controllable note segments. Picked so prefix
# growth (the server-side ``discord_audit run=N cat=... act=...``
# stem plus any future structured fields) cannot silently consume
# the budget: 512 - 128 = 384 bytes of headroom for prefix growth,
# generously above the current ~80-byte prefix.
_OPERATOR_NOTE_MAX_CHARS: Final[int] = 128

# Cc covers C0 controls (\x00-\x1f), DEL (\x7f), and C1 controls
# (\x80-\x9f). Cf covers format chars: zero-width space (U+200B),
# ZWNJ, ZWJ, LRM/RLM, bidi overrides (U+202E), etc.
_FORBIDDEN_CATEGORIES: Final[frozenset[str]] = frozenset({"Cc", "Cf"})


def safe_audit_reason(text: str) -> str:
    """
    Strip control/format characters from ``text`` and validate the byte budget.

    Raises ``ValueError`` if the post-sanitization byte length would
    exceed Discord's 512-byte cap. The audit's ``build_plan`` runs
    this helper once at plan-build time against the longest expected
    enum combination so an apply-time call cannot raise inside a
    ``transaction.atomic`` block.
    """
    cleaned = "".join(
        ch if unicodedata.category(ch) not in _FORBIDDEN_CATEGORIES else " "
        for ch in text
    )
    if len(cleaned.encode("utf-8")) > _MAX_REASON_BYTES:
        raise ValueError(
            f"audit reason exceeds {_MAX_REASON_BYTES} bytes after "
            f"sanitization; shorten or split"
        )
    return cleaned


def build_reason(
    *,
    run_id: int,
    category: str,
    action: str,
    operator_note: str = "",
) -> str:
    """
    Build a sanitized reason with explicit per-segment caps.

    The operator-controllable segment is capped at
    ``_OPERATOR_NOTE_MAX_CHARS`` characters before sanitization so
    prefix growth (future maintainers adding structured fields)
    cannot silently truncate the operator's tail.
    """
    prefix = f"{APP_LABEL} run={run_id} cat={category} act={action}"
    if operator_note:
        operator_note = operator_note[:_OPERATOR_NOTE_MAX_CHARS]
        prefix = f"{prefix} note={operator_note}"
    return safe_audit_reason(prefix)
