from abc import abstractmethod
from functools import wraps
from inspect import signature
from typing import List
from typing import Optional, Type, Callable, Any, Union, Dict, Tuple

import yaml
from pydantic import BaseModel, create_model, validate_arguments, Extra


class SchemaSettings:
    """Configuration for the pydantic model."""
    extra = Extra.forbid
    arbitrary_types_allowed = True


def extract_valid_parameters(
        inferred_type: Type[BaseModel],
        function: Callable,
) -> dict:
    """Get the arguments from a function's signature."""
    schema = inferred_type.schema()["properties"]
    valid_params = signature(function).parameters
    return {param: schema[param] for param in valid_params if param != "run_manager"}


def _construct_model_subset(
        model_name: str, original_model: BaseModel, required_fields: list
) -> Type[BaseModel]:
    """Create a pydantic model with only a subset of model's fields."""
    fields = {
        field: (
            original_model.__fields__[field].type_,
            original_model.__fields__[field].default,
        )
        for field in required_fields
        if field in original_model.__fields__
    }
    return create_model(model_name, **fields)  # type: ignore


def create_function_schema(
        schema_name: str,
        function: Callable,
) -> Type[BaseModel]:
    """Create a pydantic schema from a function's signature."""
    validated = validate_arguments(function, config=SchemaSettings)  # type: ignore
    inferred_type = validated.model  # type: ignore
    if "run_manager" in inferred_type.__fields__:
        del inferred_type.__fields__["run_manager"]
    valid_parameters = extract_valid_parameters(inferred_type, function)
    return _construct_model_subset(
        f"{schema_name}Schema", inferred_type, list(valid_parameters)
    )


class BaseToolkitConfiguration:

    def get_tool_config(key: str):
        # Default implementation of the tool configuration retrieval logic
        with open("config.yaml") as file:
            config = yaml.safe_load(file)

        # Retrieve the value associated with the given key
        return config.get(key)


class BaseTool(BaseModel):
    name: str = None
    description: str
    args_schema: Type[BaseModel] = None
    permission_required: bool = True
    toolkit_config: BaseToolkitConfiguration = BaseToolkitConfiguration()

    class Config:
        arbitrary_types_allowed = True

    @property
    def args(self):
        if self.args_schema is not None:
            return self.args_schema.schema()["properties"]
        else:
            name = self.name
            args_schema = create_function_schema(f"{name}Schema", self.execute)
            return args_schema.schema()["properties"]

    @abstractmethod
    def _execute(self, *args: Any, **kwargs: Any):
        pass

    @property
    def max_token_limit(self):
        token_limit = 600
        return token_limit

    def _parse_input(
            self,
            tool_input: Union[str, Dict],
    ) -> Union[str, Dict[str, Any]]:
        """Convert tool input to pydantic model."""
        input_args = self.args_schema
        if isinstance(tool_input, str):
            if input_args is not None:
                key_ = next(iter(input_args.__fields__.keys()))
                input_args.validate({key_: tool_input})
            return tool_input
        else:
            if input_args is not None:
                result = input_args.parse_obj(tool_input)
                return {k: v for k, v in result.dict().items() if k in tool_input}
        return tool_input

    def _to_args_and_kwargs(self, tool_input: Union[str, Dict]) -> Tuple[Tuple, Dict]:
        # For backwards compatibility, if run_input is a string,
        # pass as a positional argument.
        if isinstance(tool_input, str):
            return (tool_input,), {}
        else:
            return (), tool_input

    def execute(
            self,
            tool_input: Union[str, Dict],
            **kwargs: Any
    ) -> Any:
        """Run the tool."""
        parsed_input = self._parse_input(tool_input)

        try:
            tool_args, tool_kwargs = self._to_args_and_kwargs(parsed_input)
            observation = (
                self._execute(*tool_args, **tool_kwargs)
            )
        except (Exception, KeyboardInterrupt) as e:
            raise e
        return observation

    @classmethod
    def from_function(cls, func: Callable, args_schema: Type[BaseModel] = None):
        if args_schema:
            return cls(description=func.__doc__, args_schema=args_schema)
        else:
            return cls(description=func.__doc__)

    def get_tool_config(self, key):
        return self.toolkit_config.get_tool_config(key=key)


class FunctionalTool(BaseTool):
    name: str = None
    description: str
    func: Callable
    args_schema: Type[BaseModel] = None

    @property
    def args(self):
        if self.args_schema is not None:
            return self.args_schema.schema()["properties"]
        else:
            name = self.name
            args_schema = create_function_schema(f"{name}Schema", self.execute)
            return args_schema.schema()["properties"]

    def _execute(self, *args: Any, **kwargs: Any):
        return self.func(*args, kwargs)

    @classmethod
    def from_function(cls, func: Callable, args_schema: Type[BaseModel] = None):
        if args_schema:
            return cls(description=func.__doc__, args_schema=args_schema)
        else:
            return cls(description=func.__doc__)

    def registerTool(cls):
        cls.__registerTool__ = True
        return cls


def tool(*args: Union[str, Callable], return_direct: bool = False,
         args_schema: Optional[Type[BaseModel]] = None) -> Callable:
    def decorator(func: Callable) -> Callable:
        nonlocal args_schema

        tool_instance = FunctionalTool.from_function(func, args_schema)

        @wraps(func)
        def wrapper(*tool_args, **tool_kwargs):
            if return_direct:
                return tool_instance._exec(*tool_args, **tool_kwargs)
            else:
                return tool_instance

        return wrapper

    if len(args) == 1 and callable(args[0]):
        return decorator(args[0])
    else:
        return decorator


class BaseToolkit(BaseModel):
    name: str
    description: str

    @abstractmethod
    def get_tools(self) -> List[BaseTool]:
        # Add file related tools object here
        pass

    @abstractmethod
    def get_env_keys(self) -> List[str]:
        # Add file related config keys here
        pass
