"""Custom exceptions for the sandbox client.

All sandbox exceptions extend LangSmithError for unified error handling.
The exceptions are organized by error type rather than resource type,
with a resource_type attribute for specific handling when needed.
"""

from __future__ import annotations

from typing import Optional

from langsmith.utils import LangSmithError


class SandboxClientError(LangSmithError):
    """Base exception for sandbox client errors."""

    pass


# =============================================================================
# Connection and Authentication Errors
# =============================================================================


class SandboxAPIError(SandboxClientError):
    """Raised when the API endpoint returns an unexpected error.

    For example, this is raised for wrong URL or path.
    """

    pass


class SandboxAuthenticationError(SandboxClientError):
    """Raised when authentication fails (invalid or missing API key)."""

    pass


class SandboxConnectionError(SandboxClientError):
    """Raised when connection to the sandbox server fails."""

    pass


class SandboxRetryableConnectionError(SandboxConnectionError):
    """Raised when a transient failure occurs before a command can start.

    ``run()`` retries this error with the same command ID, so the server can
    deduplicate an attempt whose outcome is unknown.
    """

    pass


class SandboxConnectTimeoutError(SandboxRetryableConnectionError):
    """Raised when the socket fails or times out before the WebSocket handshake.

    The execute frame was never sent, so re-issuing the same command ID cannot
    double-run a command.
    """

    pass


class SandboxServerReloadError(SandboxConnectionError):
    """Raised when the server sends a 1001 Going Away close frame.

    This indicates a server hot-reload, not a true connection failure.
    The command is still running on the server.

    This is a subclass of SandboxConnectionError, so the auto-reconnect
    logic in CommandHandle catches it along with all other
    connection errors. The distinction matters for retry strategy:
    SandboxServerReloadError triggers immediate reconnect (no backoff),
    while other SandboxConnectionError triggers exponential backoff.

    Users typically never see this exception — it's handled internally.
    """

    pass


# =============================================================================
# Resource Errors (type-based, with resource_type attribute)
# =============================================================================


class ResourceNotFoundError(SandboxClientError):
    """Raised when a resource is not found.

    Attributes:
        resource_type: Type of resource (sandbox, snapshot, file).
    """

    def __init__(self, message: str, resource_type: Optional[str] = None):
        """Initialize the error."""
        super().__init__(message)
        self.resource_type = resource_type


class ResourceTimeoutError(SandboxClientError):
    """Raised when an operation times out.

    Attributes:
        resource_type: Type of resource (sandbox, snapshot).
        last_status: The last known status before timeout (for sandboxes).
    """

    def __init__(
        self,
        message: str,
        resource_type: Optional[str] = None,
        last_status: Optional[str] = None,
    ):
        """Initialize the error."""
        super().__init__(message)
        self.resource_type = resource_type
        self.last_status = last_status

    def __str__(self) -> str:
        """Return string representation."""
        base = super().__str__()
        if self.last_status:
            return f"{base} (last_status: {self.last_status})"
        return base


class ResourceInUseError(SandboxClientError):
    """Raised when deleting a resource that is still in use.

    Attributes:
        resource_type: Type of resource (snapshot).
    """

    def __init__(self, message: str, resource_type: Optional[str] = None):
        """Initialize the error."""
        super().__init__(message)
        self.resource_type = resource_type


class ResourceAlreadyExistsError(SandboxClientError):
    """Raised when creating a resource that already exists.

    Attributes:
        resource_type: Type of resource (e.g., snapshot).
    """

    def __init__(self, message: str, resource_type: Optional[str] = None):
        """Initialize the error."""
        super().__init__(message)
        self.resource_type = resource_type


class ResourceNameConflictError(SandboxClientError):
    """Raised when updating a resource name to one that already exists.

    Attributes:
        resource_type: Type of resource (sandbox, snapshot).
    """

    def __init__(self, message: str, resource_type: Optional[str] = None):
        """Initialize the error."""
        super().__init__(message)
        self.resource_type = resource_type


# =============================================================================
# Validation and Quota Errors
# =============================================================================


class ValidationError(SandboxClientError):
    """Raised when request validation fails.

    This includes:
    - Resource values exceeding server-defined limits (CPU, memory, storage)
    - Invalid resource units
    - Invalid name formats

    Attributes:
        field: The field that failed validation (e.g., "cpu", "memory").
        details: List of validation error details from the API.
        error_type: Machine-readable error type from the API.
    """

    def __init__(
        self,
        message: str,
        field: Optional[str] = None,
        details: Optional[list[dict]] = None,
        error_type: Optional[str] = None,
    ):
        """Initialize the error."""
        super().__init__(message)
        self.field = field
        self.details = details or []
        self.error_type = error_type


class QuotaExceededError(SandboxClientError):
    """Raised when organization quota limits are exceeded.

    Users should contact technical support via our Support Portal
    (https://support.langchain.com) to increase quotas.

    Attributes:
        quota_type: Type of quota exceeded (e.g., "sandbox_count", "cpu").
    """

    def __init__(self, message: str, quota_type: Optional[str] = None):
        """Initialize the error."""
        super().__init__(message)
        self.quota_type = quota_type


# =============================================================================
# Resource Creation Errors
# =============================================================================


class ResourceCreationError(SandboxClientError):
    """Raised when resource provisioning fails.

    Attributes:
        resource_type: Type of resource (sandbox, snapshot).
        error_type: Machine-readable error type (ImagePull, CrashLoop,
            SandboxConfig, Unschedulable).
    """

    def __init__(
        self,
        message: str,
        resource_type: Optional[str] = None,
        error_type: Optional[str] = None,
    ):
        """Initialize the error."""
        super().__init__(message)
        self.resource_type = resource_type
        self.error_type = error_type

    def __str__(self) -> str:
        """Return string representation."""
        if self.error_type:
            return f"{super().__str__()} [{self.error_type}]"
        return super().__str__()


# =============================================================================
# Sandbox Operation Errors (runtime errors during sandbox interaction)
# =============================================================================


class DataplaneNotConfiguredError(SandboxClientError):
    """Raised when dataplane_url is not available for the sandbox.

    This occurs when the sandbox-router URL is not configured for the cluster.
    """

    pass


class SandboxNotReadyError(SandboxClientError):
    """Raised when attempting to interact with a sandbox that is not ready."""

    pass


class SandboxOperationError(SandboxClientError):
    """Raised when a sandbox operation fails (run, read, write).

    Attributes:
        operation: The operation that failed (command, read, write).
        error_type: Machine-readable error type from the API.
    """

    def __init__(
        self,
        message: str,
        operation: Optional[str] = None,
        error_type: Optional[str] = None,
    ):
        """Initialize the error."""
        super().__init__(message)
        self.operation = operation
        self.error_type = error_type

    def __str__(self) -> str:
        """Return string representation."""
        if self.error_type:
            return f"{super().__str__()} [{self.error_type}]"
        return super().__str__()


class CommandTimeoutError(SandboxOperationError):
    """Raised when a command exceeds its timeout.

    Attributes:
        timeout: The timeout value in seconds that was exceeded.
    """

    def __init__(self, message: str, timeout: Optional[int] = None):
        """Initialize the error."""
        super().__init__(message, operation="command", error_type="CommandTimeout")
        self.timeout = timeout


# ========================================================================
# Tunnel Errors
# ========================================================================


class TunnelError(SandboxClientError):
    """Base exception for TCP tunnel errors."""

    pass


class TunnelPortNotAllowedError(TunnelError):
    """The daemon rejected the port as not allowed.

    Attributes:
        port: The port that was rejected.
    """

    def __init__(self, message: str, *, port: int):
        """Initialize the error."""
        super().__init__(message)
        self.port = port


class TunnelConnectionRefusedError(TunnelError):
    """Nothing is listening on the target port inside the sandbox.

    Attributes:
        port: The port that could not be reached.
    """

    def __init__(self, message: str, *, port: int):
        """Initialize the error."""
        super().__init__(message)
        self.port = port


class TunnelUnsupportedVersionError(TunnelError):
    """Protocol version mismatch between the tunnel client and the daemon."""

    pass
