import inspect
import os
from collections.abc import Callable
from typing import Any

from anthropic import Anthropic

from tests.fixtures.agents.prompt import SYSTEM_PROMPT
from tests.fixtures.agents.utils import (
    find_function,
    try_execute_tool,
)
from tests.fixtures.datasource import datasource


def _convert_functions_to_tools(functions: list[Callable]):
    tools = []

    for func in functions:
        properties = {}
        required = []

        for param_name, param in inspect.signature(func).parameters.items():
            if param_name in ("self", "cls"):
                continue

            param_type = param.annotation
            json_type = _python_type_to_json_type(param_type)

            properties[param_name] = {"type": json_type}

            if param.default == inspect.Parameter.empty:
                required.append(param_name)

        tools.append(
            {
                "name": func.__name__,
                "description": func.__doc__ or f"Function {func.__name__}",
                "input_schema": {
                    "type": "object",
                    "properties": properties,
                    "required": required,
                },
            }
        )

    return tools


def _get_content(response: Any) -> str:
    if (
        hasattr(response, "content")
        and response.content
        and isinstance(response.content, list)
        and len(response.content) > 0
    ):
        first_block = response.content[0]
        if hasattr(first_block, "text"):
            return first_block.text
        elif isinstance(first_block, dict) and "text" in first_block:
            return first_block["text"]
    return ""


def _python_type_to_json_type(python_type):
    type_mapping = {
        str: "string",
        int: "integer",
        float: "number",
        bool: "boolean",
        list: "array",
        dict: "object",
    }

    origin = getattr(python_type, "__origin__", None)
    if origin is not None:
        return type_mapping.get(origin, "string")

    return type_mapping.get(python_type, "string")


class AnthropicAgent:
    MAX_ITERATIONS = 3
    MODEL = "claude-sonnet-4-0"
    MAX_TOKENS = 500

    def __init__(self):
        self.client = Anthropic(
            api_key=os.getenv("ANTHROPIC_API_KEY"),
        )

    def ask(self, question: str) -> str:
        messages = [
            {
                "role": "user",
                "content": question,
            },
        ]

        for _ in range(self.MAX_ITERATIONS):
            response = self.client.messages.create(
                max_tokens=self.MAX_TOKENS,
                model=self.MODEL,
                messages=messages,
                tools=_convert_functions_to_tools(datasource),
                system=SYSTEM_PROMPT,
            )

            tool_result_messages = self._process_tool_use_block(
                response.content if hasattr(response, "content") else []
            )
            if tool_result_messages:
                messages.extend(tool_result_messages)
                continue

            return _get_content(response)
        return _get_content(response)

    async def stream(self, question: str):
        """
        Stream responses from Anthropic with tool call support.

        Args:
            question: The question to ask

        Yields:
            Text chunks from the streaming response
        """
        messages = [
            {
                "role": "user",
                "content": question,
            },
        ]

        for _ in range(self.MAX_ITERATIONS):
            with self.client.messages.stream(
                max_tokens=self.MAX_TOKENS,
                model=self.MODEL,
                messages=messages,
                tools=_convert_functions_to_tools(datasource),
                system=SYSTEM_PROMPT,
            ) as stream:
                tool_use_blocks = []
                accumulated_text = []

                for event in stream:
                    if (
                        event.type == "content_block_delta"
                        and hasattr(event.delta, "text")
                        and event.delta.text is not None
                    ):
                        accumulated_text.append(event.delta.text)
                        yield event.delta.text
                    elif (
                        event.type == "content_block_start"
                        and hasattr(event.content_block, "type")
                        and event.content_block.type == "tool_use"
                    ):
                        tool_use_blocks.append(event.content_block)
                    elif (
                        event.type == "content_block_delta"
                        and hasattr(event.delta, "type")
                        and event.delta.type == "tool_use"
                        and hasattr(event.delta, "partial_json")
                    ):
                        pass

                final_message = stream.get_final_message()
                tool_result_messages = self._process_tool_use_block(
                    final_message.content
                    if final_message and hasattr(final_message, "content")
                    else []
                )
                if tool_result_messages:
                    messages.extend(tool_result_messages)
                    continue

                break

    def _process_tool_use_block(
        self,
        content: Any,
    ) -> list[Any]:
        tool_results = []

        for block in content:
            if hasattr(block, "type") and block.type == "tool_use":
                func = find_function(datasource, block.name)

                if func:
                    result, error_msg = try_execute_tool(func, block.input)
                    tool_content = (
                        result if error_msg is None else f"Error: {error_msg}"
                    )
                    tool_results.append(
                        {
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": tool_content,
                        }
                    )

        if tool_results:
            return [
                {
                    "role": "assistant",
                    "content": content,
                },
                {
                    "role": "user",
                    "content": tool_results,
                },
            ]

        return []
