import json
from enum import Enum
from typing import TYPE_CHECKING, Dict, List, Optional, Union

# Avoid circular imports
if TYPE_CHECKING:
    from letta.schemas.message import Message


class ErrorCode(Enum):
    """Enum for error codes used by client."""

    NOT_FOUND = "NOT_FOUND"
    UNAUTHENTICATED = "UNAUTHENTICATED"
    PERMISSION_DENIED = "PERMISSION_DENIED"
    INVALID_ARGUMENT = "INVALID_ARGUMENT"
    INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR"
    CONTEXT_WINDOW_EXCEEDED = "CONTEXT_WINDOW_EXCEEDED"
    RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED"
    TIMEOUT = "TIMEOUT"
    CONFLICT = "CONFLICT"
    EXPIRED = "EXPIRED"


class LettaError(Exception):
    """Base class for all Letta related errors."""

    def __init__(self, message: str, code: Optional[ErrorCode] = None, details: Optional[Union[Dict, str, object]] = None):
        if details is None:
            details = {}
        self.message = message
        self.code = code
        self.details = details
        super().__init__(message)

    def __str__(self) -> str:
        if self.code:
            return f"{self.code.value}: {self.message}"
        return self.message

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(message='{self.message}', code='{self.code}', details={self.details})"


class PendingApprovalError(LettaError):
    """Error raised when attempting an operation while agent is waiting for tool approval."""

    def __init__(self, pending_request_id: Optional[str] = None):
        self.pending_request_id = pending_request_id
        message = "Cannot send a new message: The agent is waiting for approval on a tool call. Please approve or deny the pending request before continuing."
        code = ErrorCode.CONFLICT
        details = {"error_code": "PENDING_APPROVAL", "pending_request_id": pending_request_id}
        super().__init__(message=message, code=code, details=details)


class NoActiveRunsToCancelError(LettaError):
    """Error raised when attempting to cancel but there are no active runs to cancel."""

    def __init__(self, agent_id: Optional[str] = None):
        message = "No active runs to cancel"
        if agent_id:
            message = f"No active runs to cancel for agent {agent_id}"
        details = {"error_code": "NO_ACTIVE_RUNS_TO_CANCEL", "agent_id": agent_id}
        super().__init__(message=message, code=ErrorCode.CONFLICT, details=details)


class ConcurrentUpdateError(LettaError):
    """Error raised when a resource was updated by another transaction (optimistic locking conflict)."""

    def __init__(self, resource_type: str, resource_id: str):
        message = f"{resource_type} with id '{resource_id}' was updated by another transaction. Please retry your request."
        details = {"error_code": "CONCURRENT_UPDATE", "resource_type": resource_type, "resource_id": resource_id}
        super().__init__(message=message, code=ErrorCode.CONFLICT, details=details)


class ConversationBusyError(LettaError):
    """Error raised when attempting to send a message while another request is already processing for the same conversation."""

    def __init__(self, conversation_id: str, lock_holder_token: Optional[str] = None):
        self.conversation_id = conversation_id
        self.lock_holder_token = lock_holder_token
        message = "Cannot send a new message: Another request is currently being processed for this conversation. Please wait for the current request to complete."
        code = ErrorCode.CONFLICT
        details = {
            "error_code": "CONVERSATION_BUSY",
            "conversation_id": conversation_id,
            "lock_holder_token": lock_holder_token,
        }
        super().__init__(message=message, code=code, details=details)


class LettaToolCreateError(LettaError):
    """Error raised when a tool cannot be created."""

    default_error_message = "Error creating tool."

    def __init__(self, message=None):
        super().__init__(message=message or self.default_error_message)


class LettaToolNameConflictError(LettaError):
    """Error raised when a tool name already exists."""

    def __init__(self, tool_name: str):
        super().__init__(
            message=f"Tool with name '{tool_name}' already exists in your organization",
            code=ErrorCode.INVALID_ARGUMENT,
            details={"tool_name": tool_name},
        )


class LettaToolNameSchemaMismatchError(LettaToolCreateError):
    """Error raised when a tool name our source codedoes not match the name in the JSON schema."""

    def __init__(self, tool_name: str, json_schema_name: str, source_code: str):
        super().__init__(
            message=f"Tool name '{tool_name}' does not match the name in the JSON schema '{json_schema_name}' or in the source code `{source_code}`",
        )


class LettaConfigurationError(LettaError):
    """Error raised when there are configuration-related issues."""

    def __init__(self, message: str, missing_fields: Optional[List[str]] = None):
        self.missing_fields = missing_fields or []
        super().__init__(message=message, details={"missing_fields": self.missing_fields})


class EmbeddingConfigRequiredError(LettaError):
    """Error raised when an operation requires embedding_config but the agent doesn't have one configured."""

    def __init__(self, agent_id: Optional[str] = None, operation: Optional[str] = None):
        self.agent_id = agent_id
        self.operation = operation
        message = "This operation requires an embedding configuration, but the agent does not have one configured."
        if operation:
            message = f"Operation '{operation}' requires an embedding configuration, but the agent does not have one configured."
        details = {"agent_id": agent_id, "operation": operation}
        super().__init__(message=message, code=ErrorCode.INVALID_ARGUMENT, details=details)


class LettaAgentNotFoundError(LettaError):
    """Error raised when an agent is not found."""


class LettaUserNotFoundError(LettaError):
    """Error raised when a user is not found."""


class LettaUnsupportedFileUploadError(LettaError):
    """Error raised when an unsupported file upload is attempted."""


class LettaInvalidArgumentError(LettaError):
    """Error raised when an invalid argument is provided."""

    def __init__(self, message: str, argument_name: Optional[str] = None):
        details = {"argument_name": argument_name} if argument_name else {}
        super().__init__(message=message, code=ErrorCode.INVALID_ARGUMENT, details=details)


class LettaImageFetchError(LettaError):
    """Error raised when fetching an image from a URL fails."""

    def __init__(self, url: str, reason: str):
        details = {"url": url, "reason": reason}
        super().__init__(
            message=f"Failed to fetch image from {url}: {reason}",
            code=ErrorCode.INVALID_ARGUMENT,
            details=details,
        )


class LettaMCPError(LettaError):
    """Base error for MCP-related issues."""


class LettaInvalidMCPSchemaError(LettaMCPError):
    """Error raised when an invalid MCP schema is provided."""

    def __init__(self, server_name: str, mcp_tool_name: str, reasons: List[str]):
        details = {"server_name": server_name, "mcp_tool_name": mcp_tool_name, "reasons": reasons}
        super().__init__(
            message=f"MCP tool {mcp_tool_name} has an invalid schema and cannot be attached - reasons: {reasons}",
            code=ErrorCode.INVALID_ARGUMENT,
            details=details,
        )


class LettaMCPConnectionError(LettaMCPError):
    """Error raised when unable to connect to MCP server."""

    def __init__(self, message: str, server_name: Optional[str] = None):
        details = {"server_name": server_name} if server_name else {}
        super().__init__(message=message, code=ErrorCode.INTERNAL_SERVER_ERROR, details=details)


class LettaMCPTimeoutError(LettaMCPError):
    """Error raised when MCP server operation times out."""

    def __init__(self, message: str, server_name: Optional[str] = None):
        details = {"server_name": server_name} if server_name else {}
        super().__init__(message=message, code=ErrorCode.TIMEOUT, details=details)


class LettaServiceUnavailableError(LettaError):
    """Error raised when a required service is unavailable."""

    def __init__(self, message: str, service_name: Optional[str] = None):
        details = {"service_name": service_name} if service_name else {}
        super().__init__(message=message, code=ErrorCode.INTERNAL_SERVER_ERROR, details=details)


class LettaUnexpectedStreamCancellationError(LettaError):
    """Error raised when a streaming request is terminated unexpectedly."""


class LettaExpiredError(LettaError):
    """Error raised when a resource has expired."""

    def __init__(self, message: str):
        super().__init__(message=message, code=ErrorCode.EXPIRED)


class LLMError(LettaError):
    pass


class LLMConnectionError(LLMError):
    """Error when unable to connect to LLM service"""


class LLMRateLimitError(LLMError):
    """Error when rate limited by LLM service"""


class LLMBadRequestError(LLMError):
    """Error when LLM service cannot process request"""


class LLMAuthenticationError(LLMError):
    """Error when authentication fails with LLM service"""


class LLMPermissionDeniedError(LLMError):
    """Error when permission is denied by LLM service"""


class LLMNotFoundError(LLMError):
    """Error when requested resource is not found"""


class LLMUnprocessableEntityError(LLMError):
    """Error when request is well-formed but semantically invalid"""


class LLMServerError(LLMError):
    """Error indicating an internal server error occurred within the LLM service itself
    while processing the request."""


class LLMTimeoutError(LLMError):
    """Error when LLM request times out"""


class LLMProviderOverloaded(LLMError):
    """Error when LLM provider is overloaded"""


class BedrockPermissionError(LettaError):
    """Exception raised for errors in the Bedrock permission process."""

    def __init__(self, message="User does not have access to the Bedrock model with the specified ID."):
        super().__init__(message=message)


class BedrockError(LettaError):
    """Exception raised for errors in the Bedrock process."""

    def __init__(self, message="Error with Bedrock model."):
        super().__init__(message=message)


class LLMJSONParsingError(LettaError):
    """Exception raised for errors in the JSON parsing process."""

    def __init__(self, message="Error parsing JSON generated by LLM"):
        super().__init__(message=message)


class LocalLLMError(LettaError):
    """Generic catch-all error for local LLM problems"""

    def __init__(self, message="Encountered an error while running local LLM"):
        super().__init__(message=message)


class LocalLLMConnectionError(LettaError):
    """Error for when local LLM cannot be reached with provided IP/port"""

    def __init__(self, message="Could not connect to local LLM"):
        super().__init__(message=message)


class ContextWindowExceededError(LettaError):
    """Error raised when the context window is exceeded but further summarization fails."""

    def __init__(self, message: str, details: dict = {}):
        error_message = f"{message} ({details})"
        super().__init__(
            message=error_message,
            code=ErrorCode.CONTEXT_WINDOW_EXCEEDED,
            details=details,
        )


class SystemPromptTokenExceededError(ContextWindowExceededError):
    """Error raised when the system prompt token estimate exceeds the context window."""

    def __init__(self, system_prompt_token_estimate: int, context_window: int):
        message = f"The system prompt tokens {system_prompt_token_estimate} exceeds the context window {context_window}. Please reduce the size of your system prompt, memory blocks, or increase the context window."
        super().__init__(
            message=message, details={"system_prompt_token_estimate": system_prompt_token_estimate, "context_window": context_window}
        )


class RateLimitExceededError(LettaError):
    """Error raised when the llm rate limiter throttles api requests."""

    def __init__(self, message: str, max_retries: int):
        error_message = f"{message} ({max_retries})"
        super().__init__(
            message=error_message,
            code=ErrorCode.RATE_LIMIT_EXCEEDED,
            details={"max_retries": max_retries},
        )


class LettaMessageError(LettaError):
    """Base error class for handling message-related errors."""

    messages: List[Union["Message", "LettaMessage"]]
    default_error_message: str = "An error occurred with the message."

    def __init__(self, *, messages: List[Union["Message", "LettaMessage"]], explanation: Optional[str] = None) -> None:
        error_msg = self.construct_error_message(messages, self.default_error_message, explanation)
        super().__init__(error_msg)
        self.messages = messages

    @staticmethod
    def construct_error_message(messages: List[Union["Message", "LettaMessage"]], error_msg: str, explanation: Optional[str] = None) -> str:
        """Helper method to construct a clean and formatted error message."""
        if explanation:
            error_msg += f" (Explanation: {explanation})"

        # Pretty print out message JSON
        message_json = json.dumps([message.model_dump() for message in messages], indent=4)
        return f"{error_msg}\n\n{message_json}"


class MissingToolCallError(LettaMessageError):
    """Error raised when a message is missing a tool call."""

    default_error_message = "The message is missing a tool call."


class InvalidToolCallError(LettaMessageError):
    """Error raised when a message uses an invalid tool call."""

    default_error_message = "The message uses an invalid tool call or has improper usage of a tool call."


class MissingInnerMonologueError(LettaMessageError):
    """Error raised when a message is missing an inner monologue."""

    default_error_message = "The message is missing an inner monologue."


class InvalidInnerMonologueError(LettaMessageError):
    """Error raised when a message has a malformed inner monologue."""

    default_error_message = "The message has a malformed inner monologue."


class HandleNotFoundError(LettaError):
    """Error raised when a handle is not found."""

    def __init__(self, handle: str, available_handles: List[str]):
        super().__init__(
            message=f"Handle {handle} not found, must be one of {available_handles}",
            code=ErrorCode.NOT_FOUND,
        )


class AgentFileExportError(Exception):
    """Exception raised during agent file export operations"""


class AgentNotFoundForExportError(AgentFileExportError):
    """Exception raised when requested agents are not found during export"""

    def __init__(self, missing_ids: List[str]):
        self.missing_ids = missing_ids
        super().__init__(f"The following agent IDs were not found: {missing_ids}")


class AgentExportIdMappingError(AgentFileExportError):
    """Exception raised when ID mapping fails during export conversion"""

    def __init__(self, db_id: str, entity_type: str):
        self.db_id = db_id
        self.entity_type = entity_type
        super().__init__(
            f"Unexpected new {entity_type} ID '{db_id}' encountered during conversion. "
            f"All IDs should have been mapped during agent processing."
        )


class AgentExportProcessingError(AgentFileExportError):
    """Exception raised when general export processing fails"""

    def __init__(self, message: str, original_error: Optional[Exception] = None):
        self.original_error = original_error
        super().__init__(f"Export failed: {message}")


class AgentFileImportError(Exception):
    """Exception raised during agent file import operations"""


class RunCancelError(LettaError):
    """Error raised when a run cannot be cancelled."""

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