from abc import ABC, abstractmethod
from typing import (Any, Dict, Generic, Iterable, List, Type, TypeVar, Union,
                    cast)

from pydantic import BaseModel  # pylint: disable=no-name-in-module

from .utils import handle_errors, process_time, setup_logging

Vector = List[float]

MetaData = Dict[str, str]


logger = setup_logging(__name__)


T = TypeVar("T")



class LazyProxy(Generic[T], ABC):

    def __init__(self) -> None:

        self.__proxied: Union[T, None] = None


    def __getattr__(self, attr: str) -> object:

        return getattr(self.__get_proxied__(), attr)


    def __repr__(self) -> str:

        return repr(self.__get_proxied__())


    def __dir__(self) -> Iterable[str]:

        return self.__get_proxied__().__dir__()


    def __get_proxied__(self) -> T:

        proxied = self.__proxied

        if proxied is not None:

            return proxied


        self.__proxied = proxied = self.__load__()

        return proxied


    def __set_proxied__(self, value: T) -> None:

        self.__proxied = value


    def __as_proxied__(self) -> T:

        """Helper method that returns the current proxy, typed as the loaded object"""

        return cast(T, self)


    @abstractmethod

    def __load__(self) -> T:
        ...



class FunctionCall(BaseModel):
    name: str

    data: Any



class FunctionType(BaseModel, ABC):

    class Metadata:
        subclasses: List[Type["FunctionType"]] = []


    def __init__(self, **kwargs) -> None:

        super().__init__(**kwargs)

        logger.debug("OpenAI Function %s called", self.__class__.__name__)


    @classmethod

    def __init_subclass__(cls, **kwargs:Any):

        super().__init_subclass__(**kwargs)

        _schema = cls.schema()
        logger.debug("OpenAI Function %s schema: %s", cls.__name__, _schema)
        if cls.__doc__ is None:

            raise ValueError(

                f"FunctionType subclass {cls.__name__} must have a docstring"
            )

        cls.openaischema = {
            "name": cls.__name__,
            "description": cls.__doc__,

            "parameters": {

                "type": "object",

                "properties": {

                    k: v

                    for k, v in _schema["properties"].items()

                    if k != "self"

                },

                "required": [k for k, v in _schema["properties"].items() if v],
            },

        }

        cls.Metadata.subclasses.append(cls)


    @process_time

    @handle_errors

    async def __call__(self, **kwargs:Any) -> FunctionCall:

        response = await self.run(**kwargs)

        name = self.__class__.__name__

        return FunctionCall(name=name, data=response)


    @abstractmethod

    async def run(self, **kwargs:Any) -> Any:
        ...



F = TypeVar("F", bound=FunctionType)

