import json
from typing import Any, Union

from pydantic import BaseModel

from mlflow.exceptions import MlflowException
from mlflow.types.chat import (
    ChatMessage,
    ChatTool,
    Function,
    FunctionToolDefinition,
    ImageContentPart,
    ImageUrl,
    TextContentPart,
    ToolCall,
)
from mlflow.utils import IS_PYDANTIC_V2_OR_NEWER


def convert_message_to_mlflow_chat(message: Union[BaseModel, dict[str, Any]]) -> ChatMessage:
    """
    Convert Anthropic message object into MLflow's standard format (OpenAI compatible).

    Ref: https://docs.anthropic.com/en/api/messages#body-messages

    Args:
        message: Anthropic message object or a dictionary representing the message.

    Returns:
        ChatMessage: MLflow's standard chat message object.
    """
    if isinstance(message, dict):
        content = message.get("content")
        role = message.get("role")
    elif isinstance(message, BaseModel):
        content = message.content
        role = message.role
    else:
        raise MlflowException.invalid_parameter_value(
            f"Message must be either a dict or a Message object, but got: {type(message)}."
        )

    if isinstance(content, str):
        return ChatMessage(role=role, content=content)

    elif isinstance(content, list):
        contents = []
        tool_calls = []
        tool_call_id = None
        for content_block in content:
            if isinstance(content_block, BaseModel):
                if IS_PYDANTIC_V2_OR_NEWER:
                    content_block = content_block.model_dump()
                else:
                    content_block = content_block.dict()

            content_type = content_block.get("type")
            if content_type == "tool_use":
                # Anthropic response contains tool calls in the content block
                # Ref: https://docs.anthropic.com/en/docs/build-with-claude/tool-use#example-api-response-with-a-tool-use-content-block
                tool_calls.append(
                    ToolCall(
                        id=content_block["id"],
                        function=Function(
                            name=content_block["name"], arguments=json.dumps(content_block["input"])
                        ),
                        type="function",
                    )
                )
            elif content_type == "tool_result":
                # In Anthropic, the result of tool execution is returned as a special content type
                # "tool_result" with "user" role, which corresponds to the "tool" role in OpenAI.
                role = "tool"
                tool_call_id = content_block["tool_use_id"]
                if result_content := content_block.get("content"):
                    contents.append(_parse_content(result_content))
                else:
                    contents.append(TextContentPart(text="", type="text"))
            else:
                contents.append(_parse_content(content_block))

        message = ChatMessage(role=role, content=contents)
        # Only set tool_calls field when it is present
        if tool_calls:
            message.tool_calls = tool_calls
        if tool_call_id:
            message.tool_call_id = tool_call_id
        return message

    else:
        raise MlflowException.invalid_parameter_value(
            f"Invalid content type. Must be either a string or a list, but got: {type(content)}."
        )


def _parse_content(content: Union[str, dict[str, Any]]) -> Union[TextContentPart, ImageContentPart]:
    if isinstance(content, str):
        return TextContentPart(text=content, type="text")

    content_type = content.get("type")
    if content_type == "text":
        return TextContentPart(text=content["text"], type="text")
    elif content_type == "image":
        source = content["source"]
        return ImageContentPart(
            image_url=ImageUrl(
                url=f"data:{source['media_type']};{source['type']},{source['data']}"
            ),
            type="image_url",
        )
    # Claude 3.7 added new "thinking" content block, which is essentially a text block as of now.
    # TODO: We should consider adding a new ContentPart type if more providers support this.
    # https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking
    elif content_type == "thinking":
        return TextContentPart(text=content["thinking"], type="text")
    else:
        raise MlflowException.invalid_parameter_value(
            f"Unknown content type: {content_type['type']}. Please make sure the message "
            "is a valid Anthropic message object. If it is a valid type, contact to the "
            "MLflow maintainer via https://github.com/mlflow/mlflow/issues/new/choose for "
            "requesting support for a new message type."
        )


def convert_tool_to_mlflow_chat_tool(tool: dict[str, Any]) -> ChatTool:
    """
    Convert Anthropic tool definition into MLflow's standard format (OpenAI compatible).

    Ref: https://docs.anthropic.com/en/docs/build-with-claude/tool-use

    Args:
        tool: A dictionary represents a single tool definition in the input request.

    Returns:
        ChatTool: MLflow's standard tool definition object.
    """
    return ChatTool(
        type="function",
        function=FunctionToolDefinition(
            name=tool.get("name"),
            description=tool.get("description"),
            parameters=tool.get("input_schema"),
        ),
    )
