from __future__ import annotations

import ast
import datetime
from difflib import SequenceMatcher
import json
from json import JSONDecodeError
from textwrap import dedent
import time
from typing import TYPE_CHECKING, Any, Literal

import json5
from json_repair import repair_json  # type: ignore[import-untyped]

from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.tool_usage_events import (
    ToolSelectionErrorEvent,
    ToolUsageErrorEvent,
    ToolUsageFinishedEvent,
    ToolUsageStartedEvent,
    ToolValidateInputErrorEvent,
)
from crewai.telemetry.telemetry import Telemetry
from crewai.tools.structured_tool import CrewStructuredTool
from crewai.tools.tool_calling import InstructorToolCalling, ToolCalling
from crewai.utilities.agent_utils import (
    get_tool_names,
    render_text_description_and_args,
)
from crewai.utilities.converter import Converter
from crewai.utilities.i18n import I18N, get_i18n
from crewai.utilities.printer import Printer
from crewai.utilities.string_utils import sanitize_tool_name


if TYPE_CHECKING:
    from crewai.agents.agent_builder.base_agent import BaseAgent
    from crewai.agents.tools_handler import ToolsHandler
    from crewai.lite_agent import LiteAgent
    from crewai.llm import LLM
    from crewai.task import Task


OPENAI_BIGGER_MODELS: list[
    Literal[
        "gpt-4",
        "gpt-4o",
        "o1-preview",
        "o1-mini",
        "o1",
        "o3",
        "o3-mini",
    ]
] = [
    "gpt-4",
    "gpt-4o",
    "o1-preview",
    "o1-mini",
    "o1",
    "o3",
    "o3-mini",
]


class ToolUsageError(Exception):
    """Exception raised for errors in the tool usage."""

    def __init__(self, message: str) -> None:
        self.message = message
        super().__init__(self.message)


class ToolUsage:
    """
    Class that represents the usage of a tool by an agent.

    Attributes:
      task: Task being executed.
      tools_handler: Tools handler that will manage the tool usage.
      tools: List of tools available for the agent.
      tools_description: Description of the tools available for the agent.
      tools_names: Names of the tools available for the agent.
      function_calling_llm: Language model to be used for the tool usage.
    """

    def __init__(
        self,
        tools_handler: ToolsHandler | None,
        tools: list[CrewStructuredTool],
        task: Task | None,
        function_calling_llm: LLM,
        agent: BaseAgent | LiteAgent | None = None,
        action: Any = None,
        fingerprint_context: dict[str, str] | None = None,
    ) -> None:
        self._i18n: I18N = agent.i18n if agent else get_i18n()
        self._printer: Printer = Printer()
        self._telemetry: Telemetry = Telemetry()
        self._run_attempts: int = 1
        self._max_parsing_attempts: int = 3
        self._remember_format_after_usages: int = 3
        self.agent = agent
        self.tools_description = render_text_description_and_args(tools)
        self.tools_names = get_tool_names(tools)
        self.tools_handler = tools_handler
        self.tools = tools
        self.task = task
        self.action = action
        self.function_calling_llm = function_calling_llm
        self.fingerprint_context = fingerprint_context or {}

        # Set the maximum parsing attempts for bigger models
        if (
            self.function_calling_llm
            and self.function_calling_llm.model in OPENAI_BIGGER_MODELS
        ):
            self._max_parsing_attempts = 2
            self._remember_format_after_usages = 4

    def parse_tool_calling(
        self, tool_string: str
    ) -> ToolCalling | InstructorToolCalling | ToolUsageError:
        """Parse the tool string and return the tool calling."""
        return self._tool_calling(tool_string)

    def use(
        self, calling: ToolCalling | InstructorToolCalling, tool_string: str
    ) -> str:
        if isinstance(calling, ToolUsageError):
            error = calling.message
            if self.agent and self.agent.verbose:
                self._printer.print(content=f"\n\n{error}\n", color="red")
            if self.task:
                self.task.increment_tools_errors()
            return error

        try:
            tool = self._select_tool(calling.tool_name)
        except Exception as e:
            error = getattr(e, "message", str(e))
            if self.task:
                self.task.increment_tools_errors()
            if self.agent and self.agent.verbose:
                self._printer.print(content=f"\n\n{error}\n", color="red")
            return error

        if (
            isinstance(tool, CrewStructuredTool)
            and sanitize_tool_name(tool.name)
            == sanitize_tool_name(self._i18n.tools("add_image")["name"])  # type: ignore
        ):
            try:
                return self._use(tool_string=tool_string, tool=tool, calling=calling)

            except Exception as e:
                error = getattr(e, "message", str(e))
                if self.task:
                    self.task.increment_tools_errors()
                if self.agent and self.agent.verbose:
                    self._printer.print(content=f"\n\n{error}\n", color="red")
                return error

        return f"{self._use(tool_string=tool_string, tool=tool, calling=calling)}"

    async def ause(
        self, calling: ToolCalling | InstructorToolCalling, tool_string: str
    ) -> str:
        """Execute a tool asynchronously.

        Args:
            calling: The tool calling information.
            tool_string: The raw tool string from the agent.

        Returns:
            The result of the tool execution as a string.
        """
        if isinstance(calling, ToolUsageError):
            error = calling.message
            if self.agent and self.agent.verbose:
                self._printer.print(content=f"\n\n{error}\n", color="red")
            if self.task:
                self.task.increment_tools_errors()
            return error

        try:
            tool = self._select_tool(calling.tool_name)
        except Exception as e:
            error = getattr(e, "message", str(e))
            if self.task:
                self.task.increment_tools_errors()
            if self.agent and self.agent.verbose:
                self._printer.print(content=f"\n\n{error}\n", color="red")
            return error

        if (
            isinstance(tool, CrewStructuredTool)
            and sanitize_tool_name(tool.name)
            == sanitize_tool_name(self._i18n.tools("add_image")["name"])  # type: ignore
        ):
            try:
                return await self._ause(
                    tool_string=tool_string, tool=tool, calling=calling
                )
            except Exception as e:
                error = getattr(e, "message", str(e))
                if self.task:
                    self.task.increment_tools_errors()
                if self.agent and self.agent.verbose:
                    self._printer.print(content=f"\n\n{error}\n", color="red")
                return error

        return (
            f"{await self._ause(tool_string=tool_string, tool=tool, calling=calling)}"
        )

    async def _ause(
        self,
        tool_string: str,
        tool: CrewStructuredTool,
        calling: ToolCalling | InstructorToolCalling,
    ) -> str:
        """Internal async tool execution implementation.

        Args:
            tool_string: The raw tool string from the agent.
            tool: The tool to execute.
            calling: The tool calling information.

        Returns:
            The result of the tool execution as a string.
        """
        if self._check_tool_repeated_usage(calling=calling):
            try:
                result = self._i18n.errors("task_repeated_usage").format(
                    tool_names=self.tools_names
                )
                self._telemetry.tool_repeated_usage(
                    llm=self.function_calling_llm,
                    tool_name=sanitize_tool_name(tool.name),
                    attempts=self._run_attempts,
                )
                return self._format_result(result=result)
            except Exception:
                if self.task:
                    self.task.increment_tools_errors()

        started_at = time.time()
        started_event_emitted = False

        if self.agent:
            event_data = {
                "agent_key": self.agent.key,
                "agent_role": self.agent.role,
                "tool_name": self.action.tool,
                "tool_args": self.action.tool_input,
                "tool_class": self.action.tool,
                "agent": self.agent,
                "run_attempts": self._run_attempts,
            }

            if self.agent.fingerprint:  # type: ignore
                event_data.update(self.agent.fingerprint)  # type: ignore
            if self.task:
                event_data["task_name"] = self.task.name or self.task.description
                event_data["task_id"] = str(self.task.id)
            crewai_event_bus.emit(self, ToolUsageStartedEvent(**event_data))
            started_event_emitted = True

        from_cache = False
        result = None  # type: ignore
        should_retry = False
        available_tool = None

        try:
            if self.tools_handler and self.tools_handler.cache:
                input_str = ""
                if calling.arguments:
                    if isinstance(calling.arguments, dict):
                        input_str = json.dumps(calling.arguments)
                    else:
                        input_str = str(calling.arguments)

                result = self.tools_handler.cache.read(
                    tool=sanitize_tool_name(calling.tool_name), input=input_str
                )  # type: ignore
                from_cache = result is not None

            available_tool = next(
                (
                    available_tool
                    for available_tool in self.tools
                    if sanitize_tool_name(available_tool.name)
                    == sanitize_tool_name(tool.name)
                ),
                None,
            )

            usage_limit_error = self._check_usage_limit(
                available_tool, sanitize_tool_name(tool.name)
            )
            if usage_limit_error:
                result = usage_limit_error
                self._telemetry.tool_usage_error(llm=self.function_calling_llm)
                result = self._format_result(result=result)
                # Don't return early - fall through to finally block
            elif result is None:
                try:
                    if sanitize_tool_name(calling.tool_name) in [
                        sanitize_tool_name("Delegate work to coworker"),
                        sanitize_tool_name("Ask question to coworker"),
                    ]:
                        coworker = (
                            calling.arguments.get("coworker")
                            if calling.arguments
                            else None
                        )
                        if self.task:
                            self.task.increment_delegations(coworker)

                    if calling.arguments:
                        try:
                            acceptable_args = tool.args_schema.model_json_schema()[
                                "properties"
                            ].keys()
                            arguments = {
                                k: v
                                for k, v in calling.arguments.items()
                                if k in acceptable_args
                            }
                            arguments = self._add_fingerprint_metadata(arguments)
                            result = await tool.ainvoke(input=arguments)
                        except Exception:
                            arguments = calling.arguments
                            arguments = self._add_fingerprint_metadata(arguments)
                            result = await tool.ainvoke(input=arguments)
                    else:
                        arguments = self._add_fingerprint_metadata({})
                        result = await tool.ainvoke(input=arguments)

                    if self.tools_handler:
                        should_cache = True
                        # Check cache_function on original tool (for tools converted via to_structured_tool)
                        original_tool = getattr(available_tool, "_original_tool", None)
                        cache_func = None
                        if original_tool and hasattr(original_tool, "cache_function"):
                            cache_func = original_tool.cache_function
                        elif hasattr(available_tool, "cache_function"):
                            cache_func = available_tool.cache_function

                        if cache_func:
                            should_cache = cache_func(calling.arguments, result)

                        self.tools_handler.on_tool_use(
                            calling=calling, output=result, should_cache=should_cache
                        )

                    self._telemetry.tool_usage(
                        llm=self.function_calling_llm,
                        tool_name=sanitize_tool_name(tool.name),
                        attempts=self._run_attempts,
                    )
                    result = self._format_result(result=result)
                    data = {
                        "result": result,
                        "tool_name": sanitize_tool_name(tool.name),
                        "tool_args": calling.arguments,
                    }

                    if (
                        hasattr(available_tool, "result_as_answer")
                        and available_tool.result_as_answer
                    ):
                        result_as_answer = available_tool.result_as_answer
                        data["result_as_answer"] = result_as_answer

                    if self.agent and hasattr(self.agent, "tools_results"):
                        self.agent.tools_results.append(data)

                    if available_tool and hasattr(
                        available_tool, "_increment_usage_count"
                    ):
                        # Use _increment_usage_count to sync count to original tool
                        available_tool._increment_usage_count()
                        if (
                            hasattr(available_tool, "max_usage_count")
                            and available_tool.max_usage_count is not None
                        ):
                            self._printer.print(
                                content=f"Tool '{sanitize_tool_name(available_tool.name)}' usage: {available_tool.current_usage_count}/{available_tool.max_usage_count}",
                                color="blue",
                            )
                    elif available_tool and hasattr(
                        available_tool, "current_usage_count"
                    ):
                        available_tool.current_usage_count += 1
                        if (
                            hasattr(available_tool, "max_usage_count")
                            and available_tool.max_usage_count is not None
                        ):
                            self._printer.print(
                                content=f"Tool '{sanitize_tool_name(available_tool.name)}' usage: {available_tool.current_usage_count}/{available_tool.max_usage_count}",
                                color="blue",
                            )

                except Exception as e:
                    self.on_tool_error(tool=tool, tool_calling=calling, e=e)
                    self._run_attempts += 1
                    if self._run_attempts > self._max_parsing_attempts:
                        self._telemetry.tool_usage_error(llm=self.function_calling_llm)
                        error_message = self._i18n.errors(
                            "tool_usage_exception"
                        ).format(
                            error=e,
                            tool=sanitize_tool_name(tool.name),
                            tool_inputs=tool.description,
                        )
                        result = ToolUsageError(
                            f"\n{error_message}.\nMoving on then. {self._i18n.slice('format').format(tool_names=self.tools_names)}"
                        ).message
                        if self.task:
                            self.task.increment_tools_errors()
                        if self.agent and self.agent.verbose:
                            self._printer.print(
                                content=f"\n\n{error_message}\n", color="red"
                            )
                    else:
                        if self.task:
                            self.task.increment_tools_errors()
                        should_retry = True
            else:
                result = self._format_result(result=result)

        finally:
            if started_event_emitted:
                self.on_tool_use_finished(
                    tool=tool,
                    tool_calling=calling,
                    from_cache=from_cache,
                    started_at=started_at,
                    result=result,
                )

        # Handle retry after finally block ensures finished event was emitted
        if should_retry:
            return await self.ause(calling=calling, tool_string=tool_string)

        return result

    def _use(
        self,
        tool_string: str,
        tool: CrewStructuredTool,
        calling: ToolCalling | InstructorToolCalling,
    ) -> str:
        # Repeated usage check happens before event emission - safe to return early
        if self._check_tool_repeated_usage(calling=calling):
            try:
                result = self._i18n.errors("task_repeated_usage").format(
                    tool_names=self.tools_names
                )
                self._telemetry.tool_repeated_usage(
                    llm=self.function_calling_llm,
                    tool_name=sanitize_tool_name(tool.name),
                    attempts=self._run_attempts,
                )
                return self._format_result(result=result)

            except Exception:
                if self.task:
                    self.task.increment_tools_errors()

        started_at = time.time()
        started_event_emitted = False

        if self.agent:
            event_data = {
                "agent_key": self.agent.key,
                "agent_role": self.agent.role,
                "tool_name": self.action.tool,
                "tool_args": self.action.tool_input,
                "tool_class": self.action.tool,
                "agent": self.agent,
                "run_attempts": self._run_attempts,
            }

            # TODO: Investigate fingerprint attribute availability on BaseAgent/LiteAgent
            if self.agent.fingerprint:  # type: ignore
                event_data.update(self.agent.fingerprint)  # type: ignore
            if self.task:
                event_data["task_name"] = self.task.name or self.task.description
                event_data["task_id"] = str(self.task.id)
            crewai_event_bus.emit(self, ToolUsageStartedEvent(**event_data))
            started_event_emitted = True

        from_cache = False
        result = None  # type: ignore
        should_retry = False
        available_tool = None

        try:
            if self.tools_handler and self.tools_handler.cache:
                input_str = ""
                if calling.arguments:
                    if isinstance(calling.arguments, dict):
                        input_str = json.dumps(calling.arguments)
                    else:
                        input_str = str(calling.arguments)

                result = self.tools_handler.cache.read(
                    tool=sanitize_tool_name(calling.tool_name), input=input_str
                )  # type: ignore
                from_cache = result is not None

            available_tool = next(
                (
                    available_tool
                    for available_tool in self.tools
                    if sanitize_tool_name(available_tool.name)
                    == sanitize_tool_name(tool.name)
                ),
                None,
            )

            usage_limit_error = self._check_usage_limit(
                available_tool, sanitize_tool_name(tool.name)
            )
            if usage_limit_error:
                result = usage_limit_error
                self._telemetry.tool_usage_error(llm=self.function_calling_llm)
                result = self._format_result(result=result)
                # Don't return early - fall through to finally block
            elif result is None:
                try:
                    if sanitize_tool_name(calling.tool_name) in [
                        sanitize_tool_name("Delegate work to coworker"),
                        sanitize_tool_name("Ask question to coworker"),
                    ]:
                        coworker = (
                            calling.arguments.get("coworker")
                            if calling.arguments
                            else None
                        )
                        if self.task:
                            self.task.increment_delegations(coworker)

                    if calling.arguments:
                        try:
                            acceptable_args = tool.args_schema.model_json_schema()[
                                "properties"
                            ].keys()
                            arguments = {
                                k: v
                                for k, v in calling.arguments.items()
                                if k in acceptable_args
                            }
                            arguments = self._add_fingerprint_metadata(arguments)
                            result = tool.invoke(input=arguments)
                        except Exception:
                            arguments = calling.arguments
                            arguments = self._add_fingerprint_metadata(arguments)
                            result = tool.invoke(input=arguments)
                    else:
                        arguments = self._add_fingerprint_metadata({})
                        result = tool.invoke(input=arguments)

                    if self.tools_handler:
                        should_cache = True
                        # Check cache_function on original tool (for tools converted via to_structured_tool)
                        original_tool = getattr(available_tool, "_original_tool", None)
                        cache_func = None
                        if original_tool and hasattr(original_tool, "cache_function"):
                            cache_func = original_tool.cache_function
                        elif hasattr(available_tool, "cache_function"):
                            cache_func = available_tool.cache_function

                        if cache_func:
                            should_cache = cache_func(calling.arguments, result)

                        self.tools_handler.on_tool_use(
                            calling=calling, output=result, should_cache=should_cache
                        )

                    self._telemetry.tool_usage(
                        llm=self.function_calling_llm,
                        tool_name=sanitize_tool_name(tool.name),
                        attempts=self._run_attempts,
                    )
                    result = self._format_result(result=result)
                    data = {
                        "result": result,
                        "tool_name": sanitize_tool_name(tool.name),
                        "tool_args": calling.arguments,
                    }

                    if (
                        hasattr(available_tool, "result_as_answer")
                        and available_tool.result_as_answer
                    ):
                        result_as_answer = available_tool.result_as_answer
                        data["result_as_answer"] = result_as_answer

                    if self.agent and hasattr(self.agent, "tools_results"):
                        self.agent.tools_results.append(data)

                    if available_tool and hasattr(
                        available_tool, "_increment_usage_count"
                    ):
                        # Use _increment_usage_count to sync count to original tool
                        available_tool._increment_usage_count()
                        if (
                            hasattr(available_tool, "max_usage_count")
                            and available_tool.max_usage_count is not None
                        ):
                            self._printer.print(
                                content=f"Tool '{sanitize_tool_name(available_tool.name)}' usage: {available_tool.current_usage_count}/{available_tool.max_usage_count}",
                                color="blue",
                            )
                    elif available_tool and hasattr(
                        available_tool, "current_usage_count"
                    ):
                        available_tool.current_usage_count += 1
                        if (
                            hasattr(available_tool, "max_usage_count")
                            and available_tool.max_usage_count is not None
                        ):
                            self._printer.print(
                                content=f"Tool '{sanitize_tool_name(available_tool.name)}' usage: {available_tool.current_usage_count}/{available_tool.max_usage_count}",
                                color="blue",
                            )

                except Exception as e:
                    self.on_tool_error(tool=tool, tool_calling=calling, e=e)
                    self._run_attempts += 1
                    if self._run_attempts > self._max_parsing_attempts:
                        self._telemetry.tool_usage_error(llm=self.function_calling_llm)
                        error_message = self._i18n.errors(
                            "tool_usage_exception"
                        ).format(
                            error=e,
                            tool=sanitize_tool_name(tool.name),
                            tool_inputs=tool.description,
                        )
                        result = ToolUsageError(
                            f"\n{error_message}.\nMoving on then. {self._i18n.slice('format').format(tool_names=self.tools_names)}"
                        ).message
                        if self.task:
                            self.task.increment_tools_errors()
                        if self.agent and self.agent.verbose:
                            self._printer.print(
                                content=f"\n\n{error_message}\n", color="red"
                            )
                    else:
                        if self.task:
                            self.task.increment_tools_errors()
                        should_retry = True
            else:
                result = self._format_result(result=result)

        finally:
            if started_event_emitted:
                self.on_tool_use_finished(
                    tool=tool,
                    tool_calling=calling,
                    from_cache=from_cache,
                    started_at=started_at,
                    result=result,
                )

        # Handle retry after finally block ensures finished event was emitted
        if should_retry:
            return self.use(calling=calling, tool_string=tool_string)

        return result

    def _format_result(self, result: Any) -> str:
        if self.task:
            self.task.used_tools += 1
        if self._should_remember_format():
            result = self._remember_format(result=result)
        return str(result)

    def _should_remember_format(self) -> bool:
        if self.task:
            return self.task.used_tools % self._remember_format_after_usages == 0
        return False

    def _remember_format(self, result: str) -> str:
        result = str(result)
        result += "\n\n" + self._i18n.slice("tools").format(
            tools=self.tools_description, tool_names=self.tools_names
        )
        return result

    def _check_tool_repeated_usage(
        self, calling: ToolCalling | InstructorToolCalling
    ) -> bool:
        if not self.tools_handler:
            return False
        if last_tool_usage := self.tools_handler.last_used_tool:
            return (
                sanitize_tool_name(calling.tool_name)
                == sanitize_tool_name(last_tool_usage.tool_name)
            ) and (calling.arguments == last_tool_usage.arguments)
        return False

    @staticmethod
    def _check_usage_limit(tool: Any, tool_name: str) -> str | None:
        """Check if tool has reached its usage limit.

        Args:
            tool: The tool to check
            tool_name: The name of the tool (used for error message)

        Returns:
            Error message if limit reached, None otherwise
        """
        if (
            hasattr(tool, "max_usage_count")
            and tool.max_usage_count is not None
            and tool.current_usage_count >= tool.max_usage_count
        ):
            return f"Tool '{tool_name}' has reached its usage limit of {tool.max_usage_count} times and cannot be used anymore."
        return None

    def _select_tool(self, tool_name: str) -> Any:
        sanitized_input = sanitize_tool_name(tool_name)
        order_tools = sorted(
            self.tools,
            key=lambda tool: SequenceMatcher(
                None, sanitize_tool_name(tool.name), sanitized_input
            ).ratio(),
            reverse=True,
        )
        for tool in order_tools:
            sanitized_tool = sanitize_tool_name(tool.name)
            if (
                sanitized_tool == sanitized_input
                or SequenceMatcher(None, sanitized_tool, sanitized_input).ratio() > 0.85
            ):
                return tool
        if self.task:
            self.task.increment_tools_errors()
        tool_selection_data: dict[str, Any] = {
            "agent_key": getattr(self.agent, "key", None) if self.agent else None,
            "agent_role": getattr(self.agent, "role", None) if self.agent else None,
            "tool_name": tool_name,
            "tool_args": {},
            "tool_class": self.tools_description,
        }
        if tool_name and tool_name != "":
            error = f"Action '{tool_name}' don't exist, these are the only available Actions:\n{self.tools_description}"
            crewai_event_bus.emit(
                self,
                ToolSelectionErrorEvent(
                    **tool_selection_data,
                    error=error,
                ),
            )
            raise Exception(error)
        error = f"I forgot the Action name, these are the only available Actions: {self.tools_description}"
        crewai_event_bus.emit(
            self,
            ToolSelectionErrorEvent(
                **tool_selection_data,
                error=error,
            ),
        )
        raise Exception(error)

    def _render(self) -> str:
        """Render the tool name and description in plain text."""
        descriptions = [tool.description for tool in self.tools]
        return "\n--\n".join(descriptions)

    def _function_calling(
        self, tool_string: str
    ) -> ToolCalling | InstructorToolCalling:
        model = (
            InstructorToolCalling
            if self.function_calling_llm.supports_function_calling()
            else ToolCalling
        )
        converter = Converter(
            text=f"Only tools available:\n###\n{self._render()}\n\nReturn a valid schema for the tool, the tool name must be exactly equal one of the options, use this text to inform the valid output schema:\n\n### TEXT \n{tool_string}",
            llm=self.function_calling_llm,
            model=model,
            instructions=dedent(
                """\
        The schema should have the following structure, only two keys:
        - tool_name: str
        - arguments: dict (always a dictionary, with all arguments being passed)

        Example:
        {"tool_name": "tool name", "arguments": {"arg_name1": "value", "arg_name2": 2}}""",
            ),
            max_attempts=1,
        )
        tool_object = converter.to_pydantic()
        if not isinstance(tool_object, (ToolCalling, InstructorToolCalling)):
            raise ToolUsageError("Failed to parse tool calling")

        return tool_object

    def _original_tool_calling(
        self, tool_string: str, raise_error: bool = False
    ) -> ToolCalling | InstructorToolCalling | ToolUsageError:
        tool_name = self.action.tool
        tool = self._select_tool(tool_name)
        try:
            arguments = self._validate_tool_input(self.action.tool_input)

        except Exception:
            if raise_error:
                raise
            return ToolUsageError(f"{self._i18n.errors('tool_arguments_error')}")

        if not isinstance(arguments, dict):
            if raise_error:
                raise
            return ToolUsageError(f"{self._i18n.errors('tool_arguments_error')}")

        return ToolCalling(
            tool_name=sanitize_tool_name(tool.name),
            arguments=arguments,
        )

    def _tool_calling(
        self, tool_string: str
    ) -> ToolCalling | InstructorToolCalling | ToolUsageError:
        try:
            try:
                return self._original_tool_calling(tool_string, raise_error=True)
            except Exception:
                if self.function_calling_llm:
                    return self._function_calling(tool_string)
                return self._original_tool_calling(tool_string)
        except Exception as e:
            self._run_attempts += 1
            if self._run_attempts > self._max_parsing_attempts:
                self._telemetry.tool_usage_error(llm=self.function_calling_llm)
                if self.task:
                    self.task.increment_tools_errors()
                if self.agent and self.agent.verbose:
                    self._printer.print(content=f"\n\n{e}\n", color="red")
                return ToolUsageError(
                    f"{self._i18n.errors('tool_usage_error').format(error=e)}\nMoving on then. {self._i18n.slice('format').format(tool_names=self.tools_names)}"
                )
            return self._tool_calling(tool_string)

    def _validate_tool_input(self, tool_input: str | None) -> dict[str, Any]:
        if tool_input is None:
            return {}

        if not isinstance(tool_input, str) or not tool_input.strip():
            raise Exception(
                "Tool input must be a valid dictionary in JSON or Python literal format"
            )

        # Attempt 1: Parse as JSON
        try:
            arguments = json.loads(tool_input)
            if isinstance(arguments, dict):
                return arguments
        except (JSONDecodeError, TypeError):
            pass  # Continue to the next parsing attempt

        # Attempt 2: Parse as Python literal
        try:
            arguments = ast.literal_eval(tool_input)
            if isinstance(arguments, dict):
                return arguments
        except (ValueError, SyntaxError):
            repaired_input = repair_json(tool_input)
            # Continue to the next parsing attempt

        # Attempt 3: Parse as JSON5
        try:
            arguments = json5.loads(tool_input)
            if isinstance(arguments, dict):
                return arguments
        except (JSONDecodeError, ValueError, TypeError):
            pass  # Continue to the next parsing attempt

        # Attempt 4: Repair JSON
        try:
            repaired_input = str(repair_json(tool_input, skip_json_loads=True))
            self._printer.print(
                content=f"Repaired JSON: {repaired_input}", color="blue"
            )
            arguments = json.loads(repaired_input)
            if isinstance(arguments, dict):
                return arguments
        except Exception as e:
            error = f"Failed to repair JSON: {e}"
            self._printer.print(content=error, color="red")

        error_message = (
            "Tool input must be a valid dictionary in JSON or Python literal format"
        )
        self._emit_validate_input_error(error_message)
        # If all parsing attempts fail, raise an error
        raise Exception(error_message)

    def _emit_validate_input_error(self, final_error: str) -> None:
        tool_selection_data = {
            "agent_key": getattr(self.agent, "key", None) if self.agent else None,
            "agent_role": getattr(self.agent, "role", None) if self.agent else None,
            "tool_name": self.action.tool,
            "tool_args": str(self.action.tool_input),
            "tool_class": self.__class__.__name__,
            "agent": self.agent,  # Adding agent for fingerprint extraction
        }

        # Include fingerprint context if available
        if self.fingerprint_context:
            tool_selection_data.update(self.fingerprint_context)

        crewai_event_bus.emit(
            self,
            ToolValidateInputErrorEvent(**tool_selection_data, error=final_error),
        )

    def on_tool_error(
        self,
        tool: Any,
        tool_calling: ToolCalling | InstructorToolCalling,
        e: Exception,
    ) -> None:
        event_data = self._prepare_event_data(tool, tool_calling)
        event_data.update(
            {
                "task_id": str(self.task.id) if self.task else None,
                "task_name": self.task.name or self.task.description
                if self.task
                else None,
            }
        )
        crewai_event_bus.emit(
            self,
            ToolUsageErrorEvent(
                **{
                    **event_data,
                    "error": e,
                }
            ),
        )

    def on_tool_use_finished(
        self,
        tool: Any,
        tool_calling: ToolCalling | InstructorToolCalling,
        from_cache: bool,
        started_at: float,
        result: Any,
    ) -> None:
        finished_at = time.time()
        event_data = self._prepare_event_data(tool, tool_calling)
        event_data.update(
            {
                "started_at": datetime.datetime.fromtimestamp(started_at),
                "finished_at": datetime.datetime.fromtimestamp(finished_at),
                "from_cache": from_cache,
                "output": result,
            }
        )
        if self.task:
            event_data["task_id"] = str(self.task.id)
            event_data["task_name"] = self.task.name or self.task.description
        crewai_event_bus.emit(self, ToolUsageFinishedEvent(**event_data))

    def _prepare_event_data(
        self, tool: Any, tool_calling: ToolCalling | InstructorToolCalling
    ) -> dict[str, Any]:
        event_data = {
            "run_attempts": self._run_attempts,
            "delegations": self.task.delegations if self.task else 0,
            "tool_name": sanitize_tool_name(tool.name),
            "tool_args": tool_calling.arguments,
            "tool_class": tool.__class__.__name__,
            "agent_key": (
                getattr(self.agent, "key", "unknown") if self.agent else "unknown"
            ),
            "agent_role": (
                getattr(self.agent, "_original_role", None)
                or getattr(self.agent, "role", "unknown")
                if self.agent
                else "unknown"
            ),
        }

        # Include fingerprint context if available
        if self.fingerprint_context:
            event_data.update(self.fingerprint_context)

        return event_data

    def _add_fingerprint_metadata(self, arguments: dict[str, Any]) -> dict[str, Any]:
        """Add fingerprint metadata to tool arguments if available.

        Args:
            arguments: The original tool arguments

        Returns:
            Updated arguments dictionary with fingerprint metadata
        """
        # Create a shallow copy to avoid modifying the original
        arguments = arguments.copy()

        # Add security metadata under a designated key
        if "security_context" not in arguments:
            arguments["security_context"] = {}

        security_context = arguments["security_context"]

        # Add agent fingerprint if available
        if self.agent and hasattr(self.agent, "security_config"):
            security_config = getattr(self.agent, "security_config", None)
            if security_config and hasattr(security_config, "fingerprint"):
                try:
                    security_context["agent_fingerprint"] = (
                        security_config.fingerprint.to_dict()
                    )
                except AttributeError:
                    pass

        # Add task fingerprint if available
        if self.task and hasattr(self.task, "security_config"):
            security_config = getattr(self.task, "security_config", None)
            if security_config and hasattr(security_config, "fingerprint"):
                try:
                    security_context["task_fingerprint"] = (
                        security_config.fingerprint.to_dict()
                    )
                except AttributeError:
                    pass

        return arguments
