from typing import NoReturn


class OttoError(Exception):
    """Base exception for runtime errors surfaced by Otto."""


class OttoTransientError(OttoError):
    """Retryable runtime failures (network/timeouts/upstream 5xx)."""


class OttoExecutionError(OttoError):
    """Non-config runtime failures during request/tool execution."""


class OttoAuthError(OttoExecutionError):
    """Authentication or authorization failures against upstream services."""


class OttoConfigError(OttoError, ValueError):
    """Invalid runtime configuration."""


def classify_otto_error(exc: BaseException) -> OttoError:
    """Map arbitrary exceptions into Otto's typed error hierarchy."""
    if isinstance(exc, OttoError):
        return exc

    tokens = f"{type(exc).__name__} {exc}".lower()

    auth_markers = (
        "authentication",
        "unauthorized",
        "forbidden",
        "permission denied",
        "invalid api key",
        "expired token",
        " 401",
        " 403",
    )
    if any(marker in tokens for marker in auth_markers):
        return OttoAuthError(str(exc) or "Authentication failed")

    transient_markers = (
        "timeout",
        "timed out",
        "temporarily unavailable",
        "connection",
        "network",
        "service unavailable",
        " 429",
        " 500",
        " 502",
        " 503",
        " 504",
    )
    if any(marker in tokens for marker in transient_markers):
        return OttoTransientError(str(exc) or "Transient upstream failure")

    return OttoExecutionError(str(exc) or type(exc).__name__)


def raise_as_otto(exc: BaseException) -> NoReturn:
    """Raise a mapped OttoError while preserving the original cause."""
    mapped = classify_otto_error(exc)
    if mapped is exc:
        raise mapped
    raise mapped from exc
