import openai
from typing import List, Union
from pydantic import BaseModel, Field
from instructor import OpenAISchema
from .messages import ChainOfThought, Message, MessageRole, SystemMessage


class ChatCompletion(BaseModel):
    """
    A chat completion is a collection of messages and configration options that can be used to
    generate a chat response from the OpenAI API.


    Usage:
        In order to generate a chat response from the OpenAI API, you need to create a chat completion and then pipe it to a message and a `OpenAISchema`. Then when `create` or `acreate` is called we'll return the response from the API as an instance of `OpenAISchema`.


    Example:
        ```python
        class Sum(OpenAISchema):
            a: int
            b: int

        completion = (
            ChatCompletion("example")
            | TaggedMessage(content="What is 1 + 1?", tag="question")
            | Schema
        )

        print(completion.create())
        # Sum(a=1, b=1)
        ```


    Tips:
        * You can use the `|` operator to chain multiple messages and functions together
        * There should be exactly one function call class (OpenAISchema) per chat completion
        * System messages will be concatenated together
        * Only one chain of thought message can be used per completion


    Attributes:
        name (str): The name of the chat completion
        model (str): The model to use for the chat completion (default: "gpt-3.5-turbo-0613")
        max_tokens (int): The maximum number of tokens to generate (default: 1000)
        temperature (float): The temperature to use for the chat completion (default: 0.1)
        stream (bool): Whether to stream the response from the API (default: False)

    Warning:
        Currently we do not support streaming the response from the API, so the stream parameter is not supported yet.
    """

    name: str
    model: str = Field(default="gpt-3.5-turbo-0613")
    max_tokens: int = Field(default=1000)
    temperature: float = Field(default=0.1)
    stream: bool = Field(default=False)

    messages: List[Message] = Field(default_factory=list, repr=False)
    system_message: Message = Field(default=None, repr=False)
    cot_message: ChainOfThought = Field(default=None, repr=False)
    function: OpenAISchema = Field(default=None, repr=False)

    def __post_init__(self):
        assert self.stream == False, "Stream is not supported yet"

    def __or__(self, other: Union[Message, OpenAISchema]) -> "ChatCompletion":
        """
        Add a message or function to the chat completion, this can be used to chain multiple messages and functions together. It should contain some set of user or system messages along with a function call class (OpenAISchema)

        """

        if isinstance(other, Message):
            if other.role == MessageRole.SYSTEM:
                if not self.system_message:
                    self.system_message = other  # type: ignore
                else:
                    self.system_message.content += "\n\n" + other.content
            else:
                if isinstance(other, ChainOfThought):
                    if self.cot_message:
                        raise ValueError(
                            "Only one chain of thought message can be used per completion"
                        )
                    self.cot_message = other
                self.messages.append(other)
        else:
            if self.function:
                raise ValueError(
                    "Only one function can be used per completion, wrap your tools into a single toolkit schema"
                )
            self.function = other

            assert self.model not in {
                "gpt-3.5-turbo",
                "gpt-4",
            }, "Only *-0613 models can currently use functions"
        return self

    @property
    def kwargs(self) -> dict:
        """
        Construct the kwargs for the OpenAI API call

        Example:
            ```python
            result = openai.ChatCompletion.create(**self.kwargs)
            ```
        """
        kwargs = {}

        messages = []

        if self.system_message:
            messages.append(self.system_message.dict())

        if self.messages:
            special_types = {
                SystemMessage,
                ChainOfThought,
            }
            messages += [
                message.dict()
                for message in self.messages
                if type(message) not in special_types
            ]

        if self.cot_message:
            messages.append(self.cot_message.dict())

        kwargs["messages"] = messages

        if self.function:
            kwargs["functions"] = [self.function.openai_schema]
            kwargs["function_call"] = {"name": self.function.openai_schema["name"]}

        kwargs["max_tokens"] = self.max_tokens
        kwargs["temperature"] = self.temperature
        kwargs["model"] = self.model
        return kwargs

    def create(self):
        """
        Create a chat response from the OpenAI API

        Returns:
            response (OpenAISchema): The response from the OpenAI API
        """
        kwargs = self.kwargs
        completion = openai.ChatCompletion.create(**kwargs)
        if self.function:
            return self.function.from_response(completion)
        return completion

    async def acreate(self):
        """
        Create a chat response from the OpenAI API asynchronously

        Returns:
            response (OpenAISchema): The response from the OpenAI API
        """
        kwargs = self.kwargs
        completion = openai.ChatCompletion.acreate(**kwargs)
        if self.function:
            return self.function.from_response(await completion)
        return await completion
