from __future__ import annotations

import copy
from collections.abc import Mapping, Sequence
from typing import Any

from ember._internal.exceptions import ProviderAPIError

from ._types import MessageDict, MessageList


def _clone(value: object) -> object:
    if isinstance(value, (str, bytes, int, float, bool)) or value is None:
        return value
    try:
        return copy.deepcopy(value)
    except Exception as exc:
        raise ProviderAPIError(
            f"Message content of type {type(value).__name__} is not copyable"
        ) from exc


def normalize_context(context: object) -> MessageList | None:
    if context is None:
        return None
    if isinstance(context, (str, bytes)) or not isinstance(context, Sequence):
        raise ProviderAPIError("context must be a sequence of messages")

    normalized: list[MessageDict] = []
    for index, item in enumerate(context):
        if not isinstance(item, Mapping):
            raise ProviderAPIError(f"context entry {index} must be a mapping")
        if "role" not in item or "content" not in item:
            raise ProviderAPIError(f"context entry {index} missing 'role' or 'content'")

        normalized_message: dict[str, Any] = dict(item)
        normalized_message["role"] = str(item["role"])
        normalized_message["content"] = _clone(item["content"])
        normalized.append(normalized_message)
    return normalized


def build_messages(
    prompt: str, system_prompt: str | None, context: MessageList | None
) -> list[dict[str, object]]:
    messages: list[dict[str, object]] = []
    if system_prompt is not None:
        messages.append({"role": "system", "content": str(system_prompt)})

    if context is not None:
        for item in context:
            normalized: dict[str, Any] = dict(item)
            normalized["role"] = str(item["role"])
            normalized["content"] = _clone(item.get("content"))
            messages.append(normalized)

    # Only append prompt as user message when non-empty. When the
    # last original message was a non-user role (tool result,
    # assistant with tool_calls), context already contains it and
    # adding an empty user message would corrupt the conversation.
    if prompt:
        messages.append({"role": "user", "content": prompt})
    return messages


def as_responses_turns(messages: MessageList) -> list[dict[str, object]]:
    """Convert messages to Responses API input format.

    The Responses API uses a flat list of input items where function_call and
    function_call_output are top-level items (not nested in message content).

    Input items can be:
    - {"type": "message", "role": "...", "content": [...]}
    - {"type": "function_call", "call_id": "...", "name": "...", "arguments": "..."}
    - {"type": "function_call_output", "call_id": "...", "output": "..."}
    """
    items: list[dict[str, object]] = []

    for message in messages:
        role = str(message["role"])
        content = message.get("content", "")

        # Handle tool result messages - add as function_call_output item
        if role == "tool":
            tool_call_id = message.get("tool_call_id", "")
            items.append({
                "type": "function_call_output",
                "call_id": str(tool_call_id),
                "output": str(content) if content else "",
            })
            continue

        # Handle assistant messages that may have tool calls
        if role == "assistant":
            tool_calls = message.get("tool_calls")

            # Add message item for text content (if any)
            if content:
                items.append({
                    "type": "message",
                    "role": "assistant",
                    "content": [{"type": "output_text", "text": str(content)}],
                })

            # Add function_call items for each tool call
            if tool_calls:
                for tc in tool_calls:
                    if isinstance(tc, dict):
                        func = tc.get("function", {})
                        items.append({
                            "type": "function_call",
                            "call_id": tc.get("id", ""),
                            "name": func.get("name", "") if isinstance(func, dict) else "",
                            "arguments": func.get("arguments", "{}") if isinstance(func, dict) else "{}",
                        })
                    else:
                        func = getattr(tc, "function", None)
                        items.append({
                            "type": "function_call",
                            "call_id": getattr(tc, "id", ""),
                            "name": getattr(func, "name", "") if func else "",
                            "arguments": getattr(func, "arguments", "{}") if func else "{}",
                        })
            continue

        # Regular messages - wrap in message item
        content_type = "output_text" if role == "assistant" else "input_text"
        items.append({
            "type": "message",
            "role": role,
            "content": _coerce_responses_content(content, content_type),
        })

    return items


def _coerce_responses_content(
    content: object, content_type: str = "input_text"
) -> list[dict[str, object]]:
    if isinstance(content, str):
        return [{"type": content_type, "text": content}]
    if isinstance(content, bytes):
        return [{"type": content_type, "text": content.decode("utf-8", errors="ignore")}]
    if isinstance(content, Mapping):
        return [_coerce_responses_part(content, content_type)]
    if isinstance(content, Sequence):
        parts: list[dict[str, object]] = []
        for part in content:
            if isinstance(part, Mapping):
                parts.append(_coerce_responses_part(part, content_type))
            elif isinstance(part, str):
                parts.append({"type": content_type, "text": part})
            elif isinstance(part, bytes):
                parts.append(
                    {
                        "type": content_type,
                        "text": part.decode("utf-8", errors="ignore"),
                    }
                )
            else:
                parts.append({"type": content_type, "text": str(part)})
        return parts
    if content is None:
        return [{"type": content_type, "text": ""}]
    return [{"type": content_type, "text": str(content)}]


def _coerce_responses_part(
    part: Mapping[str, object], default_type: str = "input_text"
) -> dict[str, object]:
    converted = dict(part)
    part_type = str(converted.get("type") or default_type)
    if part_type == "text":
        converted["type"] = default_type
    return converted
