"""Transform a python function into a corresponding pydantic model.

The :class:`BackendFunction` model in this module generates subclasses based
upon a python class (similarly as the
:class:`~demessaging.backend.class_.BackendClass` does it for classes).
"""
from __future__ import annotations

import inspect
import warnings
from functools import partial
from textwrap import dedent
from typing import (
    TYPE_CHECKING,
    Any,
    Callable,
    ClassVar,
    Dict,
    Optional,
    Type,
    cast,
)

import docstring_parser
from pydantic import Field  # pylint: disable=no-name-in-module
from pydantic import BaseModel, create_model
from pydantic.json import (  # pylint: disable=no-name-in-module
    custom_pydantic_encoder,
)

import demessaging.backend.utils as utils
from demessaging.config import FunctionConfig, append_parameter_docs


def get_return_model(
    docstring: docstring_parser.Docstring, config: BackendFunctionConfig
) -> Type[BaseModel]:
    """Generate field for the return property.

    Parameters
    ----------
    docstring : docstring_parser.Docstring
        The parser that analyzed the docstring

    Returns
    -------
    Any
        The pydantic field
    """
    return_description = ""
    ret_count: int = 0
    for arg in docstring.meta:
        if (
            isinstance(arg, docstring_parser.DocstringReturns)
            and arg.description
        ):
            return_description += "\n- " + arg.description
            ret_count += 1
    return_description = return_description.strip()
    if ret_count == 1:
        return_description = return_description[2:]

    field_kws: Dict[str, Any] = {"default": None}

    if return_description.strip():
        field_kws["description"] = return_description

    field_kws.update(config.returns)

    ret_field = Field(**field_kws)  # type: ignore

    sig = config.signature

    Model: Type[BaseModel]

    create_kws: Dict[str, Any] = {}
    if config.registry.json_encoders:

        class Config:
            json_encoders = config.registry.json_encoders

        create_kws["__config__"] = Config

    if sig and sig.return_annotation is not sig.empty:
        if sig.return_annotation is None:
            create_kws["__root__"] = (Any, ret_field)
        else:
            create_kws["__root__"] = (sig.return_annotation, ret_field)
    else:
        warnings.warn(
            f"Missing return signature for {config.function.__name__}!",
            RuntimeWarning,
        )
        create_kws["__root__"] = (Any, ret_field)
    Model = create_model(
        config.class_name,
        **create_kws,  # type: ignore
    )

    return Model


@append_parameter_docs
class BackendFunctionConfig(FunctionConfig):
    """Configuration class for a backend module function."""

    function: Any = Field(description="The function to call.")

    class_name: str = Field(description="Name of the model class")

    def update_from_function(self) -> None:
        """Update the config from the corresponding function."""
        func = self.function
        if not self.name:
            self.name = func.__name__ or ""
        if not self.doc:
            self.doc = dedent(inspect.getdoc(func) or "")
        if not self.signature:
            self.signature = inspect.signature(func)


@append_parameter_docs
class BackendFunction(BaseModel):
    """A base class for a function model.

    Don't use this model, rather use :meth:`create_model` method to
    generate new models.
    """

    class Config:
        validate_assignment = True

    backend_config: ClassVar[BackendFunctionConfig]

    return_model: ClassVar[Type[BaseModel]]

    if TYPE_CHECKING:
        # added properties for subclasses generated by create_model
        func_name: str

    def __call__(self) -> BaseModel:  # type: ignore
        kws = utils.get_kws(self.backend_config.signature, self)

        for key in self.backend_config.reporter_args:
            kws[key] = getattr(self, key)

        ret = self.backend_config.function(**kws)

        return self.return_model.parse_obj(ret)

    @classmethod
    def create_model(
        cls,
        func: Callable,
        config: Optional[FunctionConfig] = None,
        class_name=None,
        **kwargs,
    ) -> Type[BackendFunction]:
        """Create a new pydantic Model from a function.

        Parameters
        ----------
        func: callable
            A function or method
        config: FunctionConfig, optional
            The configuration to use. If given, this overrides the
            ``__pulsar_config__`` of the given `func`
        class_name: str, optional
            The name for the generated subclass of :class:`pydantic.BaseModel`.
            If not given, the name of `func` is used
        ``**kwargs``
            Any other parameter for the :func:`pydantic.create_model` function

        Returns
        -------
        Subclass of BackendFunction
            The newly generated class that represents this function.
        """
        sig = inspect.signature(func)
        docstring = docstring_parser.parse(func.__doc__)  # type: ignore

        if config is None:
            config = getattr(func, "__pulsar_config__", FunctionConfig())
        config = cast(FunctionConfig, config)

        name = cast(str, func.__name__)
        if not class_name:
            class_name = utils.snake_to_camel("Func", name)

        config = BackendFunctionConfig(
            function=func,
            class_name=class_name,
            **config.copy(deep=True).dict(),
        )

        config.update_from_function()

        fields = utils.get_fields(name, sig, docstring, config)

        desc = utils.get_desc(docstring)

        kwargs.update(fields)

        Model: Type[BackendFunction] = create_model(  # type: ignore
            class_name,
            __validators__=config.validators,
            __base__=cls,
            __module__=func.__module__,
            **kwargs,  # type: ignore
        )

        Model.return_model = get_return_model(docstring, config)

        Model.backend_config = config

        if config.registry.json_encoders:
            # it would be better, to set this via __config__ in create_model,
            # but this is not possible if we use `__base__`
            Model.__config__.json_encoders = config.registry.json_encoders
            Model.__json_encoder__ = partial(
                custom_pydantic_encoder, config.registry.json_encoders
            )

        if desc:
            Model.__doc__ = desc
        else:
            Model.__doc__ = ""

        return Model


BackendFunctionConfig.update_forward_refs()
