class EdupaidError(Exception):
    """
    Base exception for Edupaid client errors.

    Used by: All Edupaid service endpoints and HTTP client.

    Attributes:
        error_details: Optional dict or string containing API error response details.
                       Edupaid API returns errors as {"error": "string"}.
        _base_message: The original message passed to the exception.
    """

    def __init__(self, message: str, error_details=None, **kwargs):
        super().__init__(message)
        self.error_details = error_details
        self._base_message = message

    def _format_error_message(self) -> str:
        """Format a unified error message with [EDUPAID] prefix."""
        parts = ["[EDUPAID]"]

        # Extract error details from the API response
        # Edupaid API returns errors as {"error": "string"}
        if self.error_details:
            if isinstance(self.error_details, dict):
                # Handle Edupaid's {"error": "string"} format
                error_msg = self.error_details.get("error")
                if error_msg:
                    parts.append(f"API Error: {error_msg}")
            elif isinstance(self.error_details, str):
                # Handle plain string error details
                parts.append(f"API Error: {self.error_details}")

        # Add the base message (contains context like endpoint, status code, etc.)
        if self._base_message:
            parts.append(self._base_message)

        return "\n".join(parts)

    def __str__(self) -> str:
        return self._format_error_message()


class ConfigurationError(EdupaidError):
    """
    Raised when required configuration/env vars are missing or invalid.

    Used by: EdupaidClient initialization when API key or base URL is missing.
    """

    def __init__(self, message: str, **kwargs):
        super().__init__(f"Configuration Error: {message}", **kwargs)


class AuthError(EdupaidError):
    """
    Raised when a 401 Unauthorized is returned by the API.

    Used by: HTTP client when API returns 401.

    Per API docs, this occurs when:
    - x-api-key header is missing
    - x-api-key header contains an invalid API key
    """

    def __init__(self, message: str, error_details=None, **kwargs):
        super().__init__(
            f"Authentication Error: {message}", error_details=error_details, **kwargs
        )


class RequestError(EdupaidError):
    """Raised for non-2xx HTTP responses not covered by more specific errors."""

    def __init__(self, message: str, error_details=None, **kwargs):
        super().__init__(
            f"Request Error: {message}", error_details=error_details, **kwargs
        )


class ValidationError(RequestError):
    """
    Raised when a 400 Bad Request is returned by the API.

    Used by: HTTP client when API returns 400 for invalid input.

    Per API docs, endpoints return 400 with {"error": "string"} for:
    - Invalid request body fields
    - Missing required fields
    - Malformed request data
    """

    def __init__(self, message: str, error_details=None, **kwargs):
        super().__init__(
            f"Validation Error: {message}", error_details=error_details, **kwargs
        )


class NotFoundError(RequestError):
    """
    Raised when a 404 Not Found is returned by the API.

    Used by: HTTP client when API returns 404.

    Per API docs, this occurs when:
    - Learning track not found or unavailable (get-app-authorization)
    - Learning track not found (batch-get-app-authorization)
    - Student or app not found, or not approved (submit_token)
    """

    def __init__(self, message: str, error_details=None, **kwargs):
        super().__init__(
            f"Resource Not Found: {message}", error_details=error_details, **kwargs
        )


class RateLimitError(RequestError):
    """
    Raised when a 429 Too Many Requests is returned by the API.

    Used by: HTTP client when API returns 429.

    Note: Not explicitly documented in API docs, but included for robust error handling.
    """

    def __init__(self, message: str, error_details=None, **kwargs):
        super().__init__(
            f"Rate Limit Exceeded: {message}", error_details=error_details, **kwargs
        )


class ServerError(RequestError):
    """
    Raised when a 5xx Server Error is returned by the API.

    Used by: HTTP client when API returns 500 or other 5xx status codes.

    Per API docs, server errors return {"error": "string"}.
    """

    def __init__(self, message: str, error_details=None, **kwargs):
        super().__init__(
            f"Server Error: {message}", error_details=error_details, **kwargs
        )


class ParseError(EdupaidError):
    """
    Raised when response parsing into Pydantic models fails.

    Used by: Endpoint utilities when API response cannot be validated
    against expected Pydantic model schema.
    """

    def __init__(self, message: str, **kwargs):
        super().__init__(f"Parse Error: {message}", **kwargs)
