"""
Custom exceptions for NEPSE Client.

This module provides a comprehensive exception hierarchy for handling
various error scenarios when interacting with the NEPSE API.
"""

from typing import Any, Dict, Optional


class NepseError(Exception):
   """
   Base exception class for all NEPSE-related errors.
   
   All custom exceptions in this library inherit from this class,
   allowing for easy catch-all error handling.
   
   Attributes:
      message: Human-readable error description
      status_code: HTTP status code (if applicable)
      response_data: Raw response data from the API
      request_data: Original request data
   """

   def __init__(
      self,
      message: str,
      status_code: Optional[int] = None,
      response_data: Optional[Any] = None,
      request_data: Optional[Dict[str, Any]] = None,
   ):
      """
      Initialize NepseError.
      
      Args:
         message: Error description
         status_code: HTTP status code
         response_data: Response from the API
         request_data: Original request data
      """
      self.message = message
      self.status_code = status_code
      self.response_data = response_data
      self.request_data = request_data
      super().__init__(self.message)

   def __str__(self) -> str:
      """Return string representation of the error."""
      base_msg = self.message
      if self.status_code:
         base_msg = f"[{self.status_code}] {base_msg}"
      return base_msg

   def __repr__(self) -> str:
      """Return detailed representation of the error."""
      return (
         f"{self.__class__.__name__}("
         f"message={self.message!r}, "
         f"status_code={self.status_code}, "
         f"response_data={self.response_data!r})"
      )

   def to_dict(self) -> Dict[str, Any]:
      """
      Convert exception to dictionary for logging/serialization.
      
      Returns:
         Dictionary representation of the exception
      """
      return {
         "error_type": self.__class__.__name__,
         "message": self.message,
         "status_code": self.status_code,
         "response_data": self.response_data,
      }


class NepseClientError(NepseError):
   """
   Raised when client sends an invalid request (4xx errors).
   
   This typically indicates:
   - Invalid parameters
   - Missing required fields
   - Malformed request data
   - Invalid company symbol
   
   Example:
      >>> try:
      ...     client.getCompanyDetails("INVALID")
      ... except NepseClientError as e:
      ...     print(f"Invalid request: {e}")
   """

   def __init__(
      self,
      message: str = "Invalid client request",
      status_code: int = 400,
      response_data: Optional[Any] = None,
      request_data: Optional[Dict[str, Any]] = None,
   ):
      super().__init__(message, status_code, response_data, request_data)


class NepseAuthenticationError(NepseError):
   """
   Raised when access token has expired (401 Unauthorized).
   
   This exception is typically handled automatically by the client,
   which will refresh the token and retry the request.
   
   Note:
      Users usually don't need to handle this exception directly
      as the client manages token refresh automatically.
   """

   def __init__(
      self,
      message: str = "Authentication token expired",
      status_code: int = 401,
      response_data: Optional[Any] = None,
   ):
      super().__init__(message, status_code, response_data)


class NepseBadGatewayError(NepseError):
   """
   Raised when server returns 502 Bad Gateway.
   
   This typically indicates:
   - Server temporarily unavailable
   - Upstream server issues
   - Network problems between servers
   
   Recommended action: Retry the request after a short delay.
   """

   def __init__(
      self,
      message: str = "Bad Gateway - Server temporarily unavailable",
      status_code: int = 502,
      response_data: Optional[Any] = None,
   ):
      super().__init__(message, status_code, response_data)


class NepseServerError(NepseError):
   """
   Generic server error for 5xx status codes.
   
   This indicates an error on the NEPSE server side.
   Common causes:
   - Internal server error (500)
   - Service unavailable (503)
   - Gateway timeout (504)
   
   Recommended action: Retry with exponential backoff or contact support.
   """

   def __init__(
      self,
      message: str = "Server error occurred",
      status_code: int = 500,
      response_data: Optional[Any] = None,
   ):
      super().__init__(message, status_code, response_data)


class NepseNetworkError(NepseError):
   """
   Raised for general network or unexpected HTTP issues.
   
   This covers:
   - Connection timeouts
   - DNS resolution failures
   - SSL/TLS errors
   - Unexpected response formats
   - Network interruptions
   
   Recommended action: Check network connectivity and retry.
   """

   def __init__(
      self,
      message: str = "Network error occurred",
      status_code: Optional[int] = None,
      response_data: Optional[Any] = None,
   ):
      super().__init__(message, status_code, response_data)


class NepseValidationError(NepseError):
   """
   Raised when input validation fails before making API request.
   
   This is raised for client-side validation errors such as:
   - Invalid date formats
   - Out of range values
   - Missing required parameters
   - Invalid data types
   
   Example:
      >>> try:
      ...     client.getCompanyPriceVolumeHistory("NABIL", start_date="invalid")
      ... except NepseValidationError as e:
      ...     print(f"Validation error: {e}")
   """

   def __init__(
      self,
      message: str = "Input validation failed",
      field: Optional[str] = None,
      value: Optional[Any] = None,
   ):
      """
      Initialize validation error.
      
      Args:
         message: Error description
         field: Name of the invalid field
         value: Invalid value provided
      """
      self.field = field
      self.value = value
      full_message = message
      if field:
         full_message = f"{message} (field: {field})"
      super().__init__(full_message)


class NepseRateLimitError(NepseError):
   """
   Raised when API rate limit is exceeded (429 Too Many Requests).
   
   Attributes:
      retry_after: Seconds to wait before retrying (if provided by server)
   """

   def __init__(
      self,
      message: str = "Rate limit exceeded",
      status_code: int = 429,
      retry_after: Optional[int] = None,
   ):
      """
      Initialize rate limit error.
      
      Args:
         message: Error description
         status_code: HTTP status code
         retry_after: Seconds to wait before retry
      """
      self.retry_after = retry_after
      if retry_after:
         message = f"{message}. Retry after {retry_after} seconds"
      super().__init__(message, status_code)


class NepseDataNotFoundError(NepseError):
   """
   Raised when requested data is not found.
   
   This is used when:
   - Company symbol doesn't exist
   - No data available for requested date
   - Empty result sets
   
   Example:
      >>> try:
      ...     client.getFloorSheetOf("INVALID", "2024-01-01")
      ... except NepseDataNotFoundError as e:
      ...     print(f"Data not found: {e}")
   """

   def __init__(
      self,
      message: str = "Requested data not found",
      resource: Optional[str] = None,
   ):
      """
      Initialize data not found error.
      
      Args:
         message: Error description
         resource: Resource that was not found
      """
      self.resource = resource
      if resource:
         message = f"{message}: {resource}"
      super().__init__(message, status_code=404)


class NepseTimeoutError(NepseError):
   """
   Raised when request times out.
   
   This occurs when the server doesn't respond within the specified timeout period.
   
   Attributes:
      timeout: Timeout value in seconds
   """

   def __init__(
      self,
      message: str = "Request timeout",
      timeout: Optional[float] = None,
   ):
      """
      Initialize timeout error.
      
      Args:
         message: Error description
         timeout: Timeout value in seconds
      """
      self.timeout = timeout
      if timeout:
         message = f"{message} after {timeout} seconds"
      super().__init__(message)


class NepseConnectionError(NepseError):
   """
   Raised when connection to NEPSE server fails.
   
   This is different from NepseNetworkError as it specifically
   indicates inability to establish a connection.
   """

   def __init__(self, message: str = "Failed to connect to NEPSE server"):
      super().__init__(message)


class NepseConfigurationError(NepseError):
   """
   Raised when there's an issue with client configuration.
   
   This includes:
   - Missing required configuration files
   - Invalid configuration values
   - Corrupted data files
   """

   def __init__(self, message: str = "Configuration error"):
      super().__init__(message)


# Exception mapping for HTTP status codes
HTTP_STATUS_EXCEPTIONS = {
   400: NepseClientError,
   401: NepseAuthenticationError,
   404: NepseDataNotFoundError,
   429: NepseRateLimitError,
   502: NepseBadGatewayError,
   503: NepseServerError,
   504: NepseServerError,
}


def get_exception_for_status(
   status_code: int,
   message: str,
   response_data: Optional[Any] = None,
) -> NepseError:
   """
   Get appropriate exception class for HTTP status code.
   
   Args:
      status_code: HTTP status code
      message: Error message
      response_data: Response data from API
      
   Returns:
      Appropriate exception instance
   """
   exception_class = HTTP_STATUS_EXCEPTIONS.get(
      status_code,
      NepseServerError if 500 <= status_code < 600 else NepseError,
   )
   return exception_class(message, status_code, response_data)


__all__ = [
   "NepseError",
   "NepseClientError",
   "NepseAuthenticationError",
   "NepseBadGatewayError",
   "NepseServerError",
   "NepseNetworkError",
   "NepseValidationError",
   "NepseRateLimitError",
   "NepseDataNotFoundError",
   "NepseTimeoutError",
   "NepseConnectionError",
   "NepseConfigurationError",
   "get_exception_for_status",
]
