"""Classify a raw yt-dlp / ffmpeg failure string into a stable category code.

The worker stores the raw exception text (truncated) for technical detail, but a raw
`ERROR: [youtube] <id>: Private video. Sign in…` is noise to a user. `classify()` maps that text
to one of a small set of category codes; the frontend turns the code into a localized, human
explanation (and decides whether a retry makes sense). The raw text stays available behind a
"details" disclosure. This is the same string-matching approach the worker already trusts for
`_RETRIABLE_MARKERS` — kept here as the single source of truth for "what kind of failure is this".
"""

# (marker, category) — first match wins, so order most-specific → most-general. Matching is
# case-insensitive on a substring, against the raw yt-dlp/ffmpeg message.
_RULES: tuple[tuple[str, str], ...] = (
    # Login / age / members / bot-check — the user must be signed in (we deliberately are not).
    ("sign in to confirm your age", "login_required"),
    ("confirm your age", "login_required"),
    ("sign in to confirm you're not a bot", "login_required"),
    ("not a bot", "login_required"),
    ("sign in to confirm", "login_required"),
    ("members-only", "login_required"),
    ("available to this channel's members", "login_required"),
    ("join this channel", "login_required"),
    ("this video is only available to", "login_required"),
    # Geo-restricted.
    ("in your country", "geo_blocked"),
    ("available in your location", "geo_blocked"),
    ("not available from your location", "geo_blocked"),
    ("geo restricted", "geo_blocked"),
    # An edit's source download went away — our own wording, and MORE specific than the generic
    # "no longer available" below, so it must be matched first.
    ("source download is no longer available", "source_missing"),
    ("source file is missing", "source_missing"),
    # Private / deleted / removed / terminated.
    ("private video", "unavailable"),
    ("video unavailable", "unavailable"),
    ("no longer available", "unavailable"),
    ("has been removed", "unavailable"),
    ("removed by the uploader", "unavailable"),
    ("account associated with this video has been terminated", "unavailable"),
    ("this video is not available", "unavailable"),
    ("this video has been removed", "unavailable"),
    # DRM.
    ("drm protected", "drm"),
    ("drm-protected", "drm"),
    # No usable media format.
    ("requested format is not available", "format_unavailable"),
    ("only images are available", "format_unavailable"),
    ("no video formats found", "format_unavailable"),
    ("unable to extract", "format_unavailable"),
    # Our own post-processing (ffmpeg) step failed.
    ("ffmpeg failed", "postprocess"),
    ("ffmpeg stalled", "postprocess"),
    ("postprocessing", "postprocess"),
    ("produced no output file", "postprocess"),
    ("no output file", "postprocess"),
    # Network / transport.
    ("unable to download", "network"),
    ("http error", "network"),
    ("timed out", "network"),
    ("read timed out", "network"),
    ("temporary failure in name resolution", "network"),
    ("connection reset", "network"),
    ("connection refused", "network"),
    ("failed to resolve", "network"),
)


def classify(msg: str | None) -> str:
    """Return a category code for a raw failure message (``"unknown"`` if nothing matches)."""
    if not msg:
        return "unknown"
    low = msg.lower()
    for marker, category in _RULES:
        if marker in low:
            return category
    return "unknown"
