"""Custom exceptions for EarthCatalog.

This module defines a hierarchy of exceptions for better error handling
and debugging throughout the EarthCatalog pipeline.

Exception Hierarchy:
    EarthCatalogError (base)
    ├── ConfigurationError
    │   ├── InvalidGridConfigError
    │   └── InvalidStorageConfigError
    ├── IngestionError
    │   ├── DownloadError
    │   ├── ValidationError
    │   └── ConsolidationError
    ├── StorageError
    │   ├── StorageConnectionError
    │   └── StorageWriteError
    └── QueryError
        └── SpatialResolverError

Usage:
    All custom exceptions inherit from EarthCatalogError, allowing users
    to catch all library-specific errors with a single except clause:

    >>> try:
    ...     pipeline.run()
    ... except EarthCatalogError as e:
    ...     logger.error(f"Pipeline failed: {e}")
    ...     print(e.details)

    Or catch specific exceptions for fine-grained handling:

    >>> try:
    ...     download_items(urls)
    ... except DownloadError as e:
    ...     logger.warning(f"Failed to download {e.url}: {e.message}")
    ... except ConsolidationError as e:
    ...     logger.error(f"Consolidation failed: {e.message}")
"""

from typing import Any


class EarthCatalogError(Exception):
    """Base exception for all EarthCatalog errors.

    All custom exceptions in EarthCatalog inherit from this class,
    allowing users to catch all library-specific errors with a single
    except clause.

    Attributes:
        message: Human-readable error description.
        details: Optional dict with additional context for debugging.

    Example:
        >>> try:
        ...     pipeline.run()
        ... except EarthCatalogError as e:
        ...     logger.error(f"Pipeline failed: {e}")
        ...     if e.details:
        ...         logger.debug(f"Details: {e.details}")
    """

    def __init__(self, message: str, details: dict[str, Any] | None = None):
        """Initialize the exception.

        Args:
            message: Human-readable error description.
            details: Optional dict with additional context.
        """
        super().__init__(message)
        self.message = message
        self.details = details or {}

    def __repr__(self) -> str:
        """Return a detailed representation of the exception."""
        return f"{self.__class__.__name__}({self.message!r})"

    def __bool__(self) -> bool:
        """Exceptions are always truthy."""
        return True


# =============================================================================
# Configuration Errors
# =============================================================================


class ConfigurationError(EarthCatalogError):
    """Error in pipeline configuration.

    Raised when configuration parameters are invalid, missing, or incompatible.

    Example:
        >>> raise ConfigurationError(
        ...     "Invalid configuration",
        ...     details={"missing_field": "output_catalog"}
        ... )
    """

    pass


class InvalidGridConfigError(ConfigurationError):
    """Invalid grid system configuration.

    Raised when grid system parameters are invalid or incompatible.

    Attributes:
        grid_system: The grid system that was configured.
        resolution: The resolution/level that was specified.

    Example:
        >>> raise InvalidGridConfigError(
        ...     "H3 resolution must be 0-15",
        ...     grid_system="h3",
        ...     resolution=20
        ... )
    """

    def __init__(
        self,
        message: str,
        grid_system: str | None = None,
        resolution: int | None = None,
        **kwargs: Any,
    ):
        """Initialize the exception.

        Args:
            message: Human-readable error description.
            grid_system: The grid system that was configured.
            resolution: The resolution/level that was specified.
            **kwargs: Additional context added to details.
        """
        details = {"grid_system": grid_system, "resolution": resolution, **kwargs}
        super().__init__(message, details)
        self.grid_system = grid_system
        self.resolution = resolution


class InvalidStorageConfigError(ConfigurationError):
    """Invalid storage backend configuration.

    Raised when storage backend parameters are invalid or the backend
    cannot be initialized.

    Attributes:
        backend: The storage backend type.
        path: The storage path that was specified.

    Example:
        >>> raise InvalidStorageConfigError(
        ...     "S3 bucket does not exist",
        ...     backend="s3",
        ...     path="s3://nonexistent-bucket/catalog"
        ... )
    """

    def __init__(
        self,
        message: str,
        backend: str | None = None,
        path: str | None = None,
        **kwargs: Any,
    ):
        """Initialize the exception.

        Args:
            message: Human-readable error description.
            backend: The storage backend type.
            path: The storage path that was specified.
            **kwargs: Additional context added to details.
        """
        details = {"backend": backend, "path": path, **kwargs}
        super().__init__(message, details)
        self.backend = backend
        self.path = path


# =============================================================================
# Ingestion Errors
# =============================================================================


class IngestionError(EarthCatalogError):
    """Error during ingestion processing.

    Base class for errors that occur during the ingestion pipeline,
    including download, validation, and consolidation phases.
    """

    pass


class DownloadError(IngestionError):
    """Error downloading STAC items.

    Raised when one or more STAC items fail to download, with details
    about the failure including HTTP status codes and retry counts.

    Attributes:
        url: The URL that failed to download.
        status_code: HTTP status code if available.
        retry_count: Number of retries attempted.
        error_type: Categorized error type (e.g., 'timeout', 'not_found').

    Example:
        >>> raise DownloadError(
        ...     "Request timed out after 30s",
        ...     url="https://api.example.com/item.json",
        ...     status_code=None,
        ...     retry_count=3,
        ...     error_type="timeout"
        ... )
    """

    def __init__(
        self,
        message: str,
        url: str | None = None,
        status_code: int | None = None,
        retry_count: int = 0,
        error_type: str | None = None,
        **kwargs: Any,
    ):
        """Initialize the exception.

        Args:
            message: Human-readable error description.
            url: The URL that failed.
            status_code: HTTP status code if available.
            retry_count: Number of retries attempted.
            error_type: Categorized error type.
            **kwargs: Additional context added to details.
        """
        details = {
            "url": url,
            "status_code": status_code,
            "retry_count": retry_count,
            "error_type": error_type,
            **kwargs,
        }
        super().__init__(message, details)
        self.url = url
        self.status_code = status_code
        self.retry_count = retry_count
        self.error_type = error_type


class ItemValidationError(IngestionError):
    """Error validating STAC item or geometry.

    Raised when a STAC item fails validation, including geometry issues,
    missing required fields, or schema violations.

    Attributes:
        item_id: The ID of the item that failed validation.
        issue_code: Validation issue code (e.g., 'INVALID_GEOMETRY').

    Example:
        >>> raise ItemValidationError(
        ...     "Geometry is self-intersecting",
        ...     item_id="ITEM_123",
        ...     issue_code="INVALID_GEOMETRY"
        ... )
    """

    def __init__(
        self,
        message: str,
        item_id: str | None = None,
        issue_code: str | None = None,
        **kwargs: Any,
    ):
        """Initialize the exception.

        Args:
            message: Human-readable error description.
            item_id: The ID of the item that failed validation.
            issue_code: Validation issue code.
            **kwargs: Additional context added to details.
        """
        details = {"item_id": item_id, "issue_code": issue_code, **kwargs}
        super().__init__(message, details)
        self.item_id = item_id
        self.issue_code = issue_code


class ConsolidationError(IngestionError):
    """Error during shard consolidation.

    Raised when the consolidation phase fails, including issues with
    reading shards, merging data, or writing final partitions.

    Attributes:
        partition_key: The partition being consolidated.
        shard_count: Number of shards being consolidated.

    Example:
        >>> raise ConsolidationError(
        ...     "Failed to merge shards: memory limit exceeded",
        ...     partition_key="h3=abc123/year=2024/month=01",
        ...     shard_count=50
        ... )
    """

    def __init__(
        self,
        message: str,
        partition_key: str | None = None,
        shard_count: int | None = None,
        **kwargs: Any,
    ):
        """Initialize the exception.

        Args:
            message: Human-readable error description.
            partition_key: The partition being consolidated.
            shard_count: Number of shards being consolidated.
            **kwargs: Additional context added to details.
        """
        details = {"partition_key": partition_key, "shard_count": shard_count, **kwargs}
        super().__init__(message, details)
        self.partition_key = partition_key
        self.shard_count = shard_count


# =============================================================================
# Storage Errors
# =============================================================================


class StorageError(EarthCatalogError):
    """Error with storage backend operations.

    Base class for errors related to storage backend operations,
    including connection failures, read/write errors, and permission issues.

    Attributes:
        path: The storage path involved in the error.
    """

    def __init__(self, message: str, path: str | None = None, **kwargs: Any):
        """Initialize the exception.

        Args:
            message: Human-readable error description.
            path: The storage path involved in the error.
            **kwargs: Additional context added to details.
        """
        details = {"path": path, **kwargs}
        super().__init__(message, details)
        self.path = path


class StorageConnectionError(StorageError):
    """Cannot connect to storage backend.

    Raised when the storage backend is unreachable or authentication fails.

    Example:
        >>> raise StorageConnectionError(
        ...     "Cannot connect to S3: access denied",
        ...     path="s3://bucket/catalog"
        ... )
    """

    pass


class StorageWriteError(StorageError):
    """Error writing to storage.

    Raised when a write operation fails, including permission issues,
    disk space, or network errors during upload.

    Attributes:
        bytes_written: Number of bytes successfully written before failure.

    Example:
        >>> raise StorageWriteError(
        ...     "Write failed: disk full",
        ...     path="/catalog/partition.parquet",
        ...     bytes_written=1024000
        ... )
    """

    def __init__(
        self,
        message: str,
        path: str | None = None,
        bytes_written: int | None = None,
        **kwargs: Any,
    ):
        """Initialize the exception.

        Args:
            message: Human-readable error description.
            path: The storage path involved in the error.
            bytes_written: Number of bytes successfully written.
            **kwargs: Additional context added to details.
        """
        super().__init__(message, path, **kwargs)
        self.bytes_written = bytes_written
        self.details["bytes_written"] = bytes_written


class StorageReadError(StorageError):
    """Error reading from storage.

    Raised when a read operation fails, including missing files,
    corrupted data, or network errors during download.

    Example:
        >>> raise StorageReadError(
        ...     "File not found",
        ...     path="s3://bucket/missing.parquet"
        ... )
    """

    pass


# =============================================================================
# Query Errors
# =============================================================================


class QueryError(EarthCatalogError):
    """Error during catalog query.

    Base class for errors related to querying catalogs, including
    spatial resolution and partition lookup failures.
    """

    pass


class SpatialResolverError(QueryError):
    """Error in spatial partition resolution.

    Raised when the spatial resolver fails to resolve partitions,
    including invalid geometries or missing schema information.

    Attributes:
        geometry_type: Type of geometry that caused the error.

    Example:
        >>> raise SpatialResolverError(
        ...     "Cannot resolve partitions: invalid geometry",
        ...     geometry_type="Polygon"
        ... )
    """

    def __init__(
        self,
        message: str,
        geometry_type: str | None = None,
        **kwargs: Any,
    ):
        """Initialize the exception.

        Args:
            message: Human-readable error description.
            geometry_type: Type of geometry that caused the error.
            **kwargs: Additional context added to details.
        """
        details = {"geometry_type": geometry_type, **kwargs}
        super().__init__(message, details)
        self.geometry_type = geometry_type
