"""
Structured errors for the orchestration layer (remote server and command catalog).

Used for: server not found, command not found, schema unavailable, missing/invalid
parameters, stale catalog, proxy discovery failure, tool-context build failure.
Validation errors are deterministic and returned before network call execution.

Author: Vasiliy Zdanovskiy
email: vasilyvz@gmail.com
"""

from typing import Any, Dict, List, Optional

from mcp_proxy_adapter.core.errors import MicroserviceError

# JSON-RPC code range for orchestration-specific errors (server error range)
ORCHESTRATION_ERROR_CODE = -32010


class OrchestrationError(MicroserviceError):
    """
    Base class for all orchestration-layer exceptions.

    Allows handlers to catch orchestration errors separately from other
    microservice errors. Subclasses carry context (e.g. server_id, command_name).
    """

    def __init__(
        self,
        message: str,
        code: int = ORCHESTRATION_ERROR_CODE,
        data: Optional[Dict[str, Any]] = None,
    ):
        """
        Initialize the orchestration error.

        Args:
            message: Human-readable error message.
            code: JSON-RPC error code (default orchestration range).
            data: Additional context (server_id, command_name, etc.).
        """
        super().__init__(message, code=code, data=data or {})


class ServerNotFoundError(OrchestrationError):
    """
    Raised when the requested server is not found in the catalog.

    Context: server_id.
    """

    def __init__(
        self,
        message: str = "Server not found in catalog",
        server_id: Optional[str] = None,
        data: Optional[Dict[str, Any]] = None,
    ):
        """
        Initialize server not found error.

        Args:
            message: Error message.
            server_id: Identifier of the server that was not found.
            data: Additional context (merged with server_id if provided).
        """
        payload = dict(data) if data else {}
        if server_id is not None:
            payload["server_id"] = server_id
        super().__init__(message, data=payload)
        self.server_id = server_id


class CommandNotFoundError(OrchestrationError):
    """
    Raised when the requested command is not found for the given server.

    Context: server_id, command_name.
    """

    def __init__(
        self,
        message: str = "Command not found for server",
        server_id: Optional[str] = None,
        command_name: Optional[str] = None,
        data: Optional[Dict[str, Any]] = None,
    ):
        """
        Initialize command not found error.

        Args:
            message: Error message.
            server_id: Identifier of the server.
            command_name: Name of the command that was not found.
            data: Additional context.
        """
        payload = dict(data) if data else {}
        if server_id is not None:
            payload["server_id"] = server_id
        if command_name is not None:
            payload["command_name"] = command_name
        super().__init__(message, data=payload)
        self.server_id = server_id
        self.command_name = command_name


class SchemaUnavailableError(OrchestrationError):
    """
    Raised when the parameter schema for a command is unavailable.

    Context: server_id, command_name.
    """

    def __init__(
        self,
        message: str = "Schema unavailable for command",
        server_id: Optional[str] = None,
        command_name: Optional[str] = None,
        data: Optional[Dict[str, Any]] = None,
    ):
        """
        Initialize schema unavailable error.

        Args:
            message: Error message.
            server_id: Identifier of the server.
            command_name: Name of the command whose schema is unavailable.
            data: Additional context.
        """
        payload = dict(data) if data else {}
        if server_id is not None:
            payload["server_id"] = server_id
        if command_name is not None:
            payload["command_name"] = command_name
        super().__init__(message, data=payload)
        self.server_id = server_id
        self.command_name = command_name


class MissingRequiredParametersError(OrchestrationError):
    """
    Raised when required parameters are missing for a command call.

    Returned before network call execution (deterministic validation).
    Context: server_id, command_name, parameter names.
    """

    def __init__(
        self,
        message: str = "Missing required parameters",
        server_id: Optional[str] = None,
        command_name: Optional[str] = None,
        parameter_names: Optional[List[str]] = None,
        data: Optional[Dict[str, Any]] = None,
    ):
        """
        Initialize missing required parameters error.

        Args:
            message: Error message.
            server_id: Identifier of the server.
            command_name: Name of the command.
            parameter_names: List of missing parameter names.
            data: Additional context.
        """
        payload = dict(data) if data else {}
        if server_id is not None:
            payload["server_id"] = server_id
        if command_name is not None:
            payload["command_name"] = command_name
        if parameter_names is not None:
            payload["parameter_names"] = list(parameter_names)
        super().__init__(message, data=payload)
        self.server_id = server_id
        self.command_name = command_name
        self.parameter_names = parameter_names or []


class InvalidParameterError(OrchestrationError):
    """
    Raised when a parameter has invalid type or shape.

    Returned before network call execution (deterministic validation).
    Context: server_id, command_name, parameter name(s), optional reason.
    """

    def __init__(
        self,
        message: str = "Invalid parameter type or shape",
        server_id: Optional[str] = None,
        command_name: Optional[str] = None,
        parameter_name: Optional[str] = None,
        reason: Optional[str] = None,
        data: Optional[Dict[str, Any]] = None,
    ):
        """
        Initialize invalid parameter error.

        Args:
            message: Error message.
            server_id: Identifier of the server.
            command_name: Name of the command.
            parameter_name: Name of the invalid parameter.
            reason: Optional reason or expected type/shape description.
            data: Additional context.
        """
        payload = dict(data) if data else {}
        if server_id is not None:
            payload["server_id"] = server_id
        if command_name is not None:
            payload["command_name"] = command_name
        if parameter_name is not None:
            payload["parameter_name"] = parameter_name
        if reason is not None:
            payload["reason"] = reason
        super().__init__(message, data=payload)
        self.server_id = server_id
        self.command_name = command_name
        self.parameter_name = parameter_name
        self.reason = reason


class StaleCatalogError(OrchestrationError):
    """
    Raised when the catalog state is stale or unavailable.

    Context: optional catalog version or reason.
    """

    def __init__(
        self,
        message: str = "Stale or unavailable catalog state",
        catalog_version: Optional[Any] = None,
        reason: Optional[str] = None,
        data: Optional[Dict[str, Any]] = None,
    ):
        """
        Initialize stale catalog error.

        Args:
            message: Error message.
            catalog_version: Optional snapshot version identifier.
            reason: Optional reason description.
            data: Additional context.
        """
        payload = dict(data) if data else {}
        if catalog_version is not None:
            payload["catalog_version"] = catalog_version
        if reason is not None:
            payload["reason"] = reason
        super().__init__(message, data=payload)
        self.catalog_version = catalog_version
        self.reason = reason


class ProxyDiscoveryError(OrchestrationError):
    """
    Raised when proxy discovery fails (e.g. cannot retrieve server list).

    Context: optional reason or proxy endpoint info.
    """

    def __init__(
        self,
        message: str = "Proxy discovery failure",
        reason: Optional[str] = None,
        data: Optional[Dict[str, Any]] = None,
    ):
        """
        Initialize proxy discovery error.

        Args:
            message: Error message.
            reason: Optional reason or detail.
            data: Additional context.
        """
        payload = dict(data) if data else {}
        if reason is not None:
            payload["reason"] = reason
        super().__init__(message, data=payload)
        self.reason = reason


class ToolContextBuildError(OrchestrationError):
    """
    Raised when building the tool context for the active catalog snapshot fails.

    Context: optional reason, snapshot version, or catalog state reference.
    """

    def __init__(
        self,
        message: str = "Tool-context build failure for active catalog snapshot",
        reason: Optional[str] = None,
        snapshot_version: Optional[Any] = None,
        data: Optional[Dict[str, Any]] = None,
    ):
        """
        Initialize tool context build error.

        Args:
            message: Error message.
            reason: Optional reason or detail.
            snapshot_version: Optional snapshot version that was being used.
            data: Additional context.
        """
        payload = dict(data) if data else {}
        if reason is not None:
            payload["reason"] = reason
        if snapshot_version is not None:
            payload["snapshot_version"] = snapshot_version
        super().__init__(message, data=payload)
        self.reason = reason
        self.snapshot_version = snapshot_version
