import base64
import json
import logging
from typing import Any, Optional, Union

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

_logger = logging.getLogger(__name__)


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

    Ref: https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Message.html

    Args:
        message: Bedrock Converse API's message object.

    Returns:
        ChatMessage: MLflow's standard chat message object.
    """
    role = message["role"]
    contents = []
    tool_calls = []
    tool_call_id = None
    for content in message["content"]:
        if tool_call := content.get("toolUse"):
            input = tool_call.get("input")
            tool_calls.append(
                ToolCall(
                    id=tool_call["toolUseId"],
                    function=Function(
                        name=tool_call["name"],
                        arguments=input if isinstance(input, str) else json.dumps(input),
                    ),
                    type="function",
                )
            )
        elif tool_result := content.get("toolResult"):
            tool_call_id = tool_result["toolUseId"]
            # "tool_result" content corresponds to the "tool" message in OpenAI.
            # https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ToolResultContentBlock.html
            role = "tool"
            for content in tool_result["content"]:
                parsed_content = _parse_content(content)
                if parsed_content:
                    contents.append(parsed_content)
        else:
            parsed_content = _parse_content(content)
            if parsed_content:
                contents.append(parsed_content)

    message = ChatMessage(role=role, content=contents)
    if tool_calls:
        message.tool_calls = tool_calls
    if tool_call_id:
        message.tool_call_id = tool_call_id
    return message


def _parse_content(content: dict[str, Any]) -> Optional[Union[TextContentPart, ImageContentPart]]:
    """
    Parse a single content block in the Bedrock message object.

    Some content types like video and document are not supported by OpenAI's spec. This
    function returns None for those content types.

    Ref: https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ContentBlock.html
    """
    if text := content.get("text"):
        return TextContentPart(text=text, type="text")
    elif json_content := content.get("json"):
        return TextContentPart(text=json.dumps(json_content), type="text")
    elif image := content.get("image"):
        # Bedrock support passing images in both raw bytes and base64 encoded strings.
        # OpenAI spec only supports base64 encoded images, so we encode the raw bytes to base64.
        # https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ImageBlock.html
        image_bytes = image["source"]["bytes"]
        if isinstance(image_bytes, bytes):
            data = base64.b64encode(image_bytes).decode("utf-8")
        else:
            data = image_bytes
        format = "image/" + image["format"]
        image_url = ImageUrl(url=f"data:{format};base64,{data}", detail="auto")
        return ImageContentPart(type="image_url", image_url=image_url)
    # NB: Video and Document content type are not supported by OpenAI's spec, so recording as text.
    else:
        _logger.debug(f"Received an unsupported content type: {list(content.keys())[0]}")
        return None


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

    Ref: https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Tool.html

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

    Returns:
        ChatTool: MLflow's standard tool definition object.
    """
    tool_spec = tool["toolSpec"]
    return ChatTool(
        type="function",
        function=FunctionToolDefinition(
            name=tool_spec["name"],
            description=tool_spec.get("description"),
            parameters=tool_spec["inputSchema"].get("json"),
        ),
    )
