"""Explicit exception handling, error codes, and secret redaction.

Provides a unified error handling layer that:

- Maps exception types to HTTP-like error categories for consistent
  CLI and API output.
- Redacts sensitive values from error details before logging
  (delegates to :mod:`cleveragents.shared.redaction`).
- Wraps unexpected exceptions in ``CleverAgentsError`` with a safe,
  user-facing message that hides internal stack details.

Usage::

    from cleveragents.core.error_handling import (
        ErrorCode,
        classify_error,
        redact_error_details,
        wrap_unexpected,
    )

    try:
        risky_operation()
    except Exception as exc:
        safe = wrap_unexpected(exc, context={"plan_id": plan_id})
        raise safe from exc
"""

from __future__ import annotations

import logging
import traceback
from enum import IntEnum
from typing import Any

from cleveragents.core.exceptions import (
    AuthenticationError,
    AuthorizationError,
    BusinessRuleViolation,
    CleverAgentsError,
    ConfigurationError,
    DatabaseError,
    DomainError,
    ExecutionError,
    ExternalServiceError,
    FileSystemError,
    InfrastructureError,
    MissingConfigurationError,
    ModelNotAvailableError,
    NetworkError,
    PlanError,
    ProviderError,
    RateLimitError,
    ResourceConflictError,
    ResourceNotFoundError,
    StreamRoutingError,
    TokenLimitExceededError,
    ValidationError,
)
from cleveragents.shared.redaction import (
    redact_dict as _shared_redact_dict,
)
from cleveragents.shared.redaction import (
    redact_value,  # re-exported
)
from cleveragents.shared.redaction import (
    register_pattern as _register_pattern,
)

__all__: list[str] = [
    "ErrorCategory",
    "ErrorCode",
    "ErrorInfo",
    "classify_error",
    "format_error_for_cli",
    "redact_error_details",
    "redact_value",
    "wrap_unexpected",
]

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Register additional secret patterns discovered during security audit.
# These extend the shared redaction module so *all* redaction paths
# (structlog, CLI, error details) benefit from the same patterns.
# ---------------------------------------------------------------------------

_register_pattern(r"eyJ[a-zA-Z0-9_-]{10,}")  # JWT tokens
_register_pattern(r"ghp_[a-zA-Z0-9]{30,}")  # GitHub PATs
_register_pattern(r"ghs_[a-zA-Z0-9]{30,}")  # GitHub App tokens
_register_pattern(r"glpat-[a-zA-Z0-9_-]{20,}")  # GitLab PATs

# ---------------------------------------------------------------------------
# Error code registry
# ---------------------------------------------------------------------------


class ErrorCode(IntEnum):
    """HTTP-like error codes for CLI output consistency.

    Maps to conventional HTTP status code ranges:
    - 400-range: client/input errors
    - 500-range: server/internal errors
    """

    # 400-range: Input / domain errors
    BAD_REQUEST = 400
    UNAUTHORIZED = 401
    FORBIDDEN = 403
    NOT_FOUND = 404
    CONFLICT = 409
    VALIDATION_FAILED = 422
    RATE_LIMITED = 429

    # 500-range: Internal / infrastructure errors
    INTERNAL = 500
    NOT_IMPLEMENTED = 501
    SERVICE_UNAVAILABLE = 503
    PROVIDER_ERROR = 520
    EXECUTION_ERROR = 521
    DATABASE_ERROR = 522
    FILESYSTEM_ERROR = 523
    NETWORK_ERROR = 524
    CONFIGURATION_ERROR = 525
    TOKEN_LIMIT = 526
    MODEL_UNAVAILABLE = 527
    STREAM_ERROR = 528


class ErrorCategory(IntEnum):
    """Broad error category for grouping."""

    CLIENT = 4
    SERVER = 5


# ---------------------------------------------------------------------------
# Exception -> ErrorCode mapping (dict-based for O(1) MRO lookup)
# ---------------------------------------------------------------------------

_EXCEPTION_CODE_MAP: dict[type[Exception], ErrorCode] = {
    # Domain / client errors
    ValidationError: ErrorCode.VALIDATION_FAILED,
    ResourceNotFoundError: ErrorCode.NOT_FOUND,
    ResourceConflictError: ErrorCode.CONFLICT,
    BusinessRuleViolation: ErrorCode.BAD_REQUEST,
    PlanError: ErrorCode.BAD_REQUEST,
    DomainError: ErrorCode.BAD_REQUEST,
    # Auth
    AuthenticationError: ErrorCode.UNAUTHORIZED,
    AuthorizationError: ErrorCode.FORBIDDEN,
    # Config
    MissingConfigurationError: ErrorCode.CONFIGURATION_ERROR,
    ConfigurationError: ErrorCode.CONFIGURATION_ERROR,
    # Provider
    RateLimitError: ErrorCode.RATE_LIMITED,
    TokenLimitExceededError: ErrorCode.TOKEN_LIMIT,
    ModelNotAvailableError: ErrorCode.MODEL_UNAVAILABLE,
    ProviderError: ErrorCode.PROVIDER_ERROR,
    # Infrastructure
    DatabaseError: ErrorCode.DATABASE_ERROR,
    NetworkError: ErrorCode.NETWORK_ERROR,
    ExternalServiceError: ErrorCode.SERVICE_UNAVAILABLE,
    FileSystemError: ErrorCode.FILESYSTEM_ERROR,
    InfrastructureError: ErrorCode.INTERNAL,
    # Execution
    StreamRoutingError: ErrorCode.STREAM_ERROR,
    ExecutionError: ErrorCode.EXECUTION_ERROR,
    # Catch-all for CleverAgentsError
    CleverAgentsError: ErrorCode.INTERNAL,
}


# ---------------------------------------------------------------------------
# Secret redaction — delegates to cleveragents.shared.redaction
# ---------------------------------------------------------------------------


def redact_error_details(details: dict[str, Any]) -> dict[str, Any]:
    """Redact sensitive values from error details.

    Delegates to :func:`cleveragents.shared.redaction.redact_dict` with
    ``show_secrets=False`` so error details are **always** redacted
    regardless of the global ``show_secrets`` flag.

    Args:
        details: The error details dict.

    Returns:
        A new dict with sensitive values redacted.
    """
    return _shared_redact_dict(details, show_secrets=False)


# ---------------------------------------------------------------------------
# Error classification
# ---------------------------------------------------------------------------


class ErrorInfo:
    """Structured error information with code and redacted details.

    Attributes:
        code: HTTP-like error code.
        category: Broad error category (client or server).
        message: User-safe error message (secrets redacted).
        exception_type: The original exception class name.
        details: Redacted error details dict.
    """

    __slots__ = ("category", "code", "details", "exception_type", "message")

    def __init__(
        self,
        *,
        code: ErrorCode,
        message: str,
        exception_type: str,
        details: dict[str, Any] | None = None,
    ) -> None:
        self.code = code
        self.category = ErrorCategory.CLIENT if code < 500 else ErrorCategory.SERVER
        self.message = message
        self.exception_type = exception_type
        self.details = details or {}


def classify_error(exc: Exception) -> ErrorInfo:
    """Classify an exception into a structured :class:`ErrorInfo`.

    Walks the exception's MRO to find the most specific match in
    ``_EXCEPTION_CODE_MAP``, redacts details **and message**, and
    returns a safe ``ErrorInfo``.

    Args:
        exc: The exception to classify.

    Returns:
        An ``ErrorInfo`` with the error code, redacted details, and
        user-safe message.
    """
    code = ErrorCode.INTERNAL
    for cls in type(exc).__mro__:
        if cls in _EXCEPTION_CODE_MAP:
            code = _EXCEPTION_CODE_MAP[cls]
            break

    details: dict[str, Any] = {}
    if isinstance(exc, CleverAgentsError) and exc.details:
        details = redact_error_details(exc.details)

    return ErrorInfo(
        code=code,
        message=redact_value(str(exc)),
        exception_type=type(exc).__name__,
        details=details,
    )


# ---------------------------------------------------------------------------
# Wrapping unexpected exceptions
# ---------------------------------------------------------------------------


def wrap_unexpected(
    exc: Exception,
    *,
    context: dict[str, Any] | None = None,
    safe_message: str = "An unexpected error occurred",
) -> CleverAgentsError:
    """Wrap an unexpected exception in ``CleverAgentsError``.

    If the exception is already a ``CleverAgentsError``, it is returned
    with optional context merged into a **copy** of its details (the
    original ``details`` dict object is not mutated).

    For non-``CleverAgentsError`` exceptions, a new error is created
    with the *safe_message* (hiding internal details from users).
    The original exception type and a sanitised traceback snippet are
    stored in the ``details`` dict for diagnostics.

    Args:
        exc: The exception to wrap.
        context: Optional extra context (e.g. ``plan_id``,
            ``project_name``) to merge into the error details.
        safe_message: User-facing message when wrapping unknown
            exceptions.

    Returns:
        A ``CleverAgentsError`` instance.
    """
    extra_context = redact_error_details(context) if context else {}

    if isinstance(exc, CleverAgentsError):
        if extra_context:
            # Create a new dict so the original details object is not mutated.
            exc.details = {**exc.details, **extra_context}
        return exc

    # Build sanitised details for diagnostics.
    tb_lines = traceback.format_exception(type(exc), exc, exc.__traceback__)
    tb_text = "".join(tb_lines[-3:])  # last 3 lines only
    tb_text = redact_value(tb_text)

    details: dict[str, Any] = {
        "exception_type": type(exc).__name__,
        "traceback_snippet": tb_text,
    }
    details.update(extra_context)

    logger.error(
        "Wrapping unexpected exception: %s: %s",
        type(exc).__name__,
        redact_value(str(exc)),
    )

    return CleverAgentsError(safe_message, details=details)


# ---------------------------------------------------------------------------
# CLI formatting
# ---------------------------------------------------------------------------


def format_error_for_cli(info: ErrorInfo) -> str:
    """Format an ``ErrorInfo`` for human-readable CLI output.

    Args:
        info: The structured error info.

    Returns:
        Multi-line string suitable for terminal display.
    """
    lines: list[str] = [
        f"Error [{info.code.value}] {info.code.name}: {info.message}",
    ]
    if info.details:
        for key, value in info.details.items():
            lines.append(f"  {key}: {value}")
    return "\n".join(lines)
