"""SDKRouter exceptions with helpful error messages."""

from __future__ import annotations

import functools
import logging
from contextlib import contextmanager
from typing import Any, Callable, TypeVar, ParamSpec

import httpx

logger = logging.getLogger(__name__)

P = ParamSpec("P")
T = TypeVar("T")


class SDKRouterError(Exception):
    """Base exception for all SDKRouter errors."""

    def __init__(
        self,
        message: str,
        *,
        error_code: str | None = None,
        status_code: int | None = None,
        suggestion: str | None = None,
        original_error: Exception | None = None,
    ):
        self.message = message
        self.error_code = error_code
        self.status_code = status_code
        self.suggestion = suggestion
        self.original_error = original_error

        # Build full message
        full_message = message
        if suggestion:
            full_message = f"{message}\n\nSuggestion: {suggestion}"

        super().__init__(full_message)

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(message={self.message!r}, error_code={self.error_code!r})"


class APIError(SDKRouterError):
    """Error from API response."""

    pass


class ImageFetchError(SDKRouterError):
    """Server failed to fetch image from URL.

    This happens when:
    - The image URL is not accessible from the server
    - The server has network issues (firewall, DNS, timeout)
    - The image host blocks server requests

    Solution: Use image_path to upload local file instead.
    """

    pass


class AuthenticationError(SDKRouterError):
    """Invalid or missing API key."""

    pass


class RateLimitError(SDKRouterError):
    """Rate limit exceeded."""

    pass


class ValidationError(SDKRouterError):
    """Invalid request parameters."""

    pass


class TimeoutError(SDKRouterError):
    """Request timed out."""

    pass


class NetworkError(SDKRouterError):
    """Network connection error."""

    pass


class NotFoundError(SDKRouterError):
    """Resource or endpoint not found (404).

    This usually means:
    - The API endpoint doesn't exist (wrong URL)
    - The resource (UUID) doesn't exist
    - The server is not configured correctly

    For local development, set:
        SDKROUTER_API_URL=http://127.0.0.1:8000
    """

    pass


# Error code to exception class mapping
ERROR_CODE_MAP: dict[str, type[SDKRouterError]] = {
    "IMAGE_FETCH_ERROR": ImageFetchError,
    "AUTHENTICATION_ERROR": AuthenticationError,
    "INVALID_API_KEY": AuthenticationError,
    "RATE_LIMIT_EXCEEDED": RateLimitError,
    "VALIDATION_ERROR": ValidationError,
}

# Suggestions for specific error codes
ERROR_SUGGESTIONS: dict[str, str] = {
    "IMAGE_FETCH_ERROR": (
        "The server couldn't fetch the image from URL. Try using image_path instead:\n"
        "  result = client.vision.analyze(\n"
        "      image_path=Path('./your_image.jpg'),  # Upload directly\n"
        "      prompt='Describe this image',\n"
        "  )"
    ),
    "AUTHENTICATION_ERROR": (
        "Check your API key:\n"
        "  client = SDKRouter(api_key='your-api-key')\n"
        "Or set SDKROUTER_API_KEY environment variable."
    ),
    "INVALID_API_KEY": (
        "Check your API key:\n"
        "  client = SDKRouter(api_key='your-api-key')\n"
        "Or set SDKROUTER_API_KEY environment variable."
    ),
    "RATE_LIMIT_EXCEEDED": "Wait a moment and retry, or contact support to increase your rate limit.",
    "NOT_FOUND": (
        "The API endpoint was not found. This usually means:\n"
        "1. The endpoint is not deployed on the server yet\n"
        "2. You're using production URL but need to test locally\n\n"
        "For local development, set:\n"
        "  export SDKROUTER_API_URL=http://127.0.0.1:8000\n"
        "Or:\n"
        "  client = SDKRouter(api_url='http://127.0.0.1:8000')"
    ),
}


def parse_api_error(
    status_code: int,
    error_body: dict[str, Any] | str,
    original_error: Exception | None = None,
) -> SDKRouterError:
    """Parse API error response and return appropriate exception.

    Args:
        status_code: HTTP status code
        error_body: Error response body (dict or string)
        original_error: Original httpx exception

    Returns:
        Appropriate SDKRouterError subclass
    """
    # Extract error code and message
    if isinstance(error_body, dict):
        error_code = error_body.get("error") or error_body.get("code")
        # error_code must be a string for dict lookup; nested dicts are not valid codes
        if not isinstance(error_code, str):
            error_code = None
        message = error_body.get("message") or error_body.get("detail") or str(error_body)
    else:
        error_code = None
        message = str(error_body)

    # Get suggestion for this error code
    suggestion = ERROR_SUGGESTIONS.get(error_code) if error_code else None

    # Map to specific exception class
    exception_class = APIError
    if error_code and error_code in ERROR_CODE_MAP:
        exception_class = ERROR_CODE_MAP[error_code]
    elif status_code == 401:
        exception_class = AuthenticationError
        suggestion = suggestion or ERROR_SUGGESTIONS.get("AUTHENTICATION_ERROR")
    elif status_code == 429:
        exception_class = RateLimitError
        suggestion = suggestion or ERROR_SUGGESTIONS.get("RATE_LIMIT_EXCEEDED")
    elif status_code == 404:
        exception_class = NotFoundError
        # For HTML 404 pages, provide a cleaner message
        if isinstance(error_body, str) and "<!doctype" in error_body.lower():
            message = "Endpoint not found (404)"
        suggestion = suggestion or ERROR_SUGGESTIONS.get("NOT_FOUND")
    elif status_code == 422:
        exception_class = ValidationError

    return exception_class(
        message=message,
        error_code=error_code,
        status_code=status_code,
        suggestion=suggestion,
        original_error=original_error,
    )


@contextmanager
def handle_api_errors():
    """Context manager to catch and convert httpx errors to SDK exceptions.

    Usage:
        with handle_api_errors():
            result = self._api.analyze_create(request)
        return result
    """
    try:
        yield
    except httpx.HTTPStatusError as e:
        # Parse error body
        try:
            error_body = e.response.json()
        except Exception:
            error_body = e.response.text

        logger.debug("API error: status=%d, body=%s", e.response.status_code, error_body)
        raise parse_api_error(
            status_code=e.response.status_code,
            error_body=error_body,
            original_error=e,
        ) from e
    except httpx.TimeoutException as e:
        logger.debug("Timeout error: %s", e)
        raise TimeoutError(
            message="Request timed out",
            suggestion="Try increasing the timeout: SDKRouter(timeout=120.0)",
            original_error=e,
        ) from e
    except httpx.ConnectError as e:
        logger.debug("Connection error: %s", e)
        raise NetworkError(
            message=f"Failed to connect: {e}",
            suggestion="Check your internet connection and API URL.",
            original_error=e,
        ) from e
    except httpx.RequestError as e:
        logger.debug("Request error: %s", e)
        raise NetworkError(
            message=f"Network error: {e}",
            original_error=e,
        ) from e


def api_error_handler(func: Callable[P, T]) -> Callable[P, T]:
    """Decorator to wrap API calls with error handling.

    Usage:
        @api_error_handler
        def analyze(self, *, image_url: str) -> VisionAnalyzeResponse:
            return self._api.analyze_create(request)
    """
    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        with handle_api_errors():
            return func(*args, **kwargs)
    return wrapper


def async_api_error_handler(func: Callable[..., Any]) -> Callable[..., Any]:
    """Decorator to wrap async API calls with error handling.

    Usage:
        @async_api_error_handler
        async def analyze(self, *, image_url: str) -> VisionAnalyzeResponse:
            return await self._api.analyze_create(request)
    """
    @functools.wraps(func)
    async def wrapper(*args: Any, **kwargs: Any) -> Any:
        try:
            return await func(*args, **kwargs)
        except httpx.HTTPStatusError as e:
            try:
                error_body = e.response.json()
            except Exception:
                error_body = e.response.text

            logger.debug("API error: status=%d, body=%s", e.response.status_code, error_body)
            raise parse_api_error(
                status_code=e.response.status_code,
                error_body=error_body,
                original_error=e,
            ) from e
        except httpx.TimeoutException as e:
            logger.debug("Timeout error: %s", e)
            raise TimeoutError(
                message="Request timed out",
                suggestion="Try increasing the timeout: SDKRouter(timeout=120.0)",
                original_error=e,
            ) from e
        except httpx.ConnectError as e:
            logger.debug("Connection error: %s", e)
            raise NetworkError(
                message=f"Failed to connect: {e}",
                suggestion="Check your internet connection and API URL.",
                original_error=e,
            ) from e
        except httpx.RequestError as e:
            logger.debug("Request error: %s", e)
            raise NetworkError(
                message=f"Network error: {e}",
                original_error=e,
            ) from e
    return wrapper


__all__ = [
    # Exception classes
    "SDKRouterError",
    "APIError",
    "ImageFetchError",
    "AuthenticationError",
    "RateLimitError",
    "ValidationError",
    "TimeoutError",
    "NetworkError",
    "NotFoundError",
    # Utilities
    "parse_api_error",
    "handle_api_errors",
    "api_error_handler",
    "async_api_error_handler",
    "ERROR_CODE_MAP",
    "ERROR_SUGGESTIONS",
]
