"""
Base command classes for MCP Microservice.
"""

import inspect
from abc import ABC, abstractmethod
from typing import TypeVar, Type, ClassVar, Dict, Any, List

from docstring_parser import parse

from mcp_proxy_adapter.core.errors import (
    CommandError,
    InternalError,
    InvalidParamsError,
    NotFoundError,
    ValidationError,
)
from mcp_proxy_adapter.core.logging import get_global_logger
from mcp_proxy_adapter.commands.result import ErrorResult


class CommandResult:
    """Base class for command results."""
    
    def __init__(self, success: bool = True, data: dict = None, error: str = None):
        """
        Initialize command result.
        
        Args:
            success: Whether the command executed successfully
            data: Result data dictionary
            error: Error message if command failed
        """
        self.success = success
        self.data = data or {}
        self.error = error
    
    def to_dict(self) -> dict:
        """Convert to dictionary."""
        result = {"success": self.success}
        if self.data:
            result["data"] = self.data
        if self.error:
            result["error"] = self.error
        return result


T = TypeVar("T", bound=CommandResult)


class Command(ABC):
    """
    Base abstract class for all commands.
    """

    # Command name for registration
    name: ClassVar[str]
    # Command version (default: 0.1)
    version: ClassVar[str] = "0.1"
    # Plugin filename
    plugin: ClassVar[str] = ""
    # Command description
    descr: ClassVar[str] = ""
    # Command category
    category: ClassVar[str] = ""
    # Command author
    author: ClassVar[str] = ""
    # Author email
    email: ClassVar[str] = ""
    # Source URL
    source_url: ClassVar[str] = ""
    # Result class
    result_class: ClassVar[Type[CommandResult]]
    # Use queue for execution (if True, command will be executed via queue and return job_id)
    use_queue: ClassVar[bool] = False

    @abstractmethod
    async def execute(self, **kwargs) -> CommandResult:
        """
        Execute command with the specified parameters.

        Args:
            **kwargs: Command parameters including optional 'context' parameter.

        Returns:
            Command result.
        """
        pass

    @classmethod
    def get_schema(cls) -> Dict[str, Any]:
        """
        Get JSON schema for command parameters.

        Returns:
            JSON schema with metadata including use_queue flag.
        """
        schema = {"type": "object", "properties": {}, "additionalProperties": True}
        # Add metadata about queue usage
        if hasattr(cls, "use_queue") and cls.use_queue:
            schema["x-use-queue"] = True
        return schema

    @classmethod
    def get_result_schema(cls) -> Dict[str, Any]:
        """
        Get JSON schema for command result.

        Returns:
            JSON schema.
        """
        if hasattr(cls, "result_class") and cls.result_class:
            return cls.result_class.get_schema()
        return {}

    def validate_params(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """
        Validate command parameters.

        Args:
            params: Parameters to validate.

        Returns:
            Validated parameters.

        Raises:
            ValidationError: If parameters are invalid.
        """
        # Ensure params is a dictionary, even if None was passed
        if params is None:
            params = {}

        # Create a copy to avoid modifying the input dictionary during iteration
        validated_params = params.copy()

        # Handle None values and empty strings in parameters
        for key, value in list(validated_params.items()):
            # Process None values or empty strings - this helps with JavaScript null/undefined conversions
            if value is None or (
                isinstance(value, str) and value.lower() in ["null", "none", ""]
            ):
                # For commands that specifically handle None values, keep the parameter
                # (like help), keep the parameter but ensure it's a proper Python None
                if key in [
                    "cmdname"
                ]:  # список параметров, для которых None является допустимым значением
                    validated_params[key] = None
                else:
                    # For most parameters, remove None values to avoid issues
                    del validated_params[key]

        # Get command schema to validate parameters
        schema = self.get_schema()
        if schema and "properties" in schema:
            allowed_properties = schema["properties"].keys()
            # Check additionalProperties setting (default: False for strict validation)
            additional_properties_allowed = schema.get("additionalProperties", False)

            # Find parameters that are not in the schema
            invalid_params = []
            for param_name in list(validated_params.keys()):
                if param_name not in allowed_properties:
                    invalid_params.append(param_name)

            # Handle invalid parameters based on additionalProperties setting
            if invalid_params:
                if additional_properties_allowed:
                    # Permissive mode: allow additional parameters, just log debug info
                    get_global_logger().debug(
                        f"Command {self.__class__.__name__} received additional parameters: {invalid_params}. "
                        f"These are allowed due to additionalProperties: true"
                    )
                else:
                    # Strict mode: raise ValidationError for invalid parameters
                    raise ValidationError(
                        f"Invalid parameters: {', '.join(invalid_params)}. "
                        f"Allowed parameters: {list(allowed_properties)}",
                        data={"invalid_parameters": invalid_params},
                    )

        # Validate required parameters based on command schema
        if schema and "required" in schema:
            required_params = schema["required"]
            missing_params = []

            for param in required_params:
                if param not in validated_params:
                    missing_params.append(param)

            if missing_params:
                raise ValidationError(
                    f"Missing required parameters: {', '.join(missing_params)}",
                    data={"missing_parameters": missing_params},
                )

        return validated_params

    @classmethod
    async def run(cls, **kwargs) -> CommandResult:
        """
        Runs the command with the specified arguments.

        Args:
            **kwargs: Command arguments including optional 'context' parameter.

        Returns:
            Command result.
        """
        # Extract context from kwargs
        context = kwargs.pop("context", {}) if "context" in kwargs else {}

        try:
            get_global_logger().debug(f"Running command {cls.__name__} with params: {kwargs}")

            # Import registry here to avoid circular imports
            from mcp_proxy_adapter.commands.command_registry import registry

            # Get command name
            if not hasattr(cls, "name") or not cls.name:
                command_name = cls.__name__.lower()
                if command_name.endswith("command"):
                    command_name = command_name[:-7]
            else:
                command_name = cls.name

            # Ensure kwargs is never None
            if kwargs is None:
                kwargs = {}

            # Get command with priority (custom commands first, then built-in)
            command_class = registry.get_command(command_name)
            if command_class is None:
                raise NotFoundError(f"Command '{command_name}' not found")

            # Create new instance and validate parameters
            command = command_class()
            validated_params = command.validate_params(kwargs)

            # Execute command with validated parameters and context
            result = await command.execute(**validated_params, context=context)

            get_global_logger().debug(f"Command {cls.__name__} executed successfully")
            return result
        except ValidationError as e:
            # Ошибка валидации параметров
            get_global_logger().error(f"Validation error in command {cls.__name__}: {e}")
            return ErrorResult(message=str(e), code=e.code, details=e.data)
        except InvalidParamsError as e:
            # Ошибка в параметрах команды
            get_global_logger().error(f"Invalid parameters error in command {cls.__name__}: {e}")
            return ErrorResult(message=str(e), code=e.code, details=e.data)
        except NotFoundError as e:
            # Ресурс не найден
            get_global_logger().error(f"Resource not found error in command {cls.__name__}: {e}")
            return ErrorResult(message=str(e), code=e.code, details=e.data)
        except TimeoutError as e:
            # Превышено время ожидания
            get_global_logger().error(f"Timeout error in command {cls.__name__}: {e}")
            return ErrorResult(message=str(e), code=e.code, details=e.data)
        except CommandError as e:
            # Ошибка выполнения команды
            get_global_logger().error(f"Command error in {cls.__name__}: {e}")
            return ErrorResult(message=str(e), code=e.code, details=e.data)
        except Exception as e:
            # Непредвиденная ошибка
            get_global_logger().exception(f"Unexpected error executing command {cls.__name__}: {e}")
            internal_error = InternalError(f"Command execution error: {str(e)}")
            return ErrorResult(
                message=internal_error.message,
                code=internal_error.code,
                details={"original_error": str(e)},
            )

    @classmethod
    def get_param_info(cls) -> Dict[str, Dict[str, Any]]:
        """
        Gets information about execute method parameters.

        Returns:
            Dictionary with parameters information.
        """
        signature = inspect.signature(cls.execute)
        params = {}

        for name, param in signature.parameters.items():
            if name == "self":
                continue

            param_info = {
                "name": name,
                "required": param.default == inspect.Parameter.empty,
            }

            if param.annotation != inspect.Parameter.empty:
                param_info["type"] = str(param.annotation)

            if param.default != inspect.Parameter.empty:
                param_info["default"] = param.default

            params[name] = param_info

        return params

    @classmethod

    @classmethod
    def _generate_examples(
        cls, params: Dict[str, Dict[str, Any]]
    ) -> List[Dict[str, Any]]:
        """
        Generates usage examples of the command based on its parameters.

        Args:
            params: Information about command parameters

        Returns:
            List of examples
        """
        examples = []

        # Simple example without parameters, if all parameters are optional
        if not any(param.get("required", False) for param in params.values()):
            examples.append(
                {
                    "command": cls.name,
                    "description": f"Call {cls.name} command without parameters",
                }
            )

        # Example with all required parameters
        required_params = {k: v for k, v in params.items() if v.get("required", False)}
        if required_params:
            sample_params = {}
            for param_name, param_info in required_params.items():
                # Try to generate sample value based on type
                param_type = param_info.get("type", "")

                if "str" in param_type:
                    sample_params[param_name] = f"sample_{param_name}"
                elif "int" in param_type:
                    sample_params[param_name] = 1
                elif "float" in param_type:
                    sample_params[param_name] = 1.0
                elif "bool" in param_type:
                    sample_params[param_name] = True
                elif "list" in param_type or "List" in param_type:
                    sample_params[param_name] = []
                elif "dict" in param_type or "Dict" in param_type:
                    sample_params[param_name] = {}
                else:
                    sample_params[param_name] = "..."

            examples.append(
                {
                    "command": cls.name,
                    "params": sample_params,
                    "description": f"Call {cls.name} command with required parameters",
                }
            )

        # Example with all parameters (including optional ones)
        if len(params) > len(required_params):
            all_params = {}
            for param_name, param_info in params.items():
                # For required parameters, use the same values as above
                if param_info.get("required", False):
                    # Try to generate sample value based on type
                    param_type = param_info.get("type", "")

                    if "str" in param_type:
                        all_params[param_name] = f"sample_{param_name}"
                    elif "int" in param_type:
                        all_params[param_name] = 1
                    elif "float" in param_type:
                        all_params[param_name] = 1.0
                    elif "bool" in param_type:
                        all_params[param_name] = True
                    elif "list" in param_type or "List" in param_type:
                        all_params[param_name] = []
                    elif "dict" in param_type or "Dict" in param_type:
                        all_params[param_name] = {}
                    else:
                        all_params[param_name] = "..."
                # For optional parameters, use their default values or a sample value
                else:
                    if "default" in param_info:
                        all_params[param_name] = param_info["default"]
                    else:
                        param_type = param_info.get("type", "")

                        if "str" in param_type:
                            all_params[param_name] = f"optional_{param_name}"
                        elif "int" in param_type:
                            all_params[param_name] = 0
                        elif "float" in param_type:
                            all_params[param_name] = 0.0
                        elif "bool" in param_type:
                            all_params[param_name] = False
                        elif "list" in param_type or "List" in param_type:
                            all_params[param_name] = []
                        elif "dict" in param_type or "Dict" in param_type:
                            all_params[param_name] = {}
                        else:
                            all_params[param_name] = None

            examples.append(
                {
                    "command": cls.name,
                    "params": all_params,
                    "description": f"Call {cls.name} command with all parameters",
                }
            )

        return examples
