# Copyright (c) Microsoft. All rights reserved.

"""Tool call formatters for displaying function calls in the harness console.

This module provides formatters that convert raw function call content into
human-readable display strings. Each formatter handles specific tool patterns
(e.g., web_search, todos_*, etc.) and the FallbackToolFormatter provides
generic formatting for any unmatched tools.

Usage:
    from harness.console.formatters import build_default_formatters, format_tool_call
    from agent_framework import Content

    call = Content.from_function_call(
        call_id="call_1",
        name="web_search",
        arguments={"query": "Python async"}
    )
    formatters = build_default_formatters()
    result = format_tool_call(formatters, call)  # "web_search (Python async)"
"""

from __future__ import annotations

import contextlib
import json
from abc import ABC, abstractmethod
from typing import Any

from agent_framework import Content

# region Helper Functions


def get_argument_value(call: Content, param_name: str) -> Any:
    """Extract an argument value from a function call.

    Handles both dict and JSON string arguments.

    Args:
        call: The function call content.
        param_name: The parameter name to extract.

    Returns:
        The argument value, or None if not found.
    """
    if call.arguments is None:
        return None

    if isinstance(call.arguments, str):
        # arguments is a JSON string, parse it
        try:
            args_dict = json.loads(call.arguments)
        except (json.JSONDecodeError, TypeError):
            return None
        if not isinstance(args_dict, dict):
            return None
    elif isinstance(call.arguments, dict):
        args_dict = call.arguments
    else:
        return None

    return args_dict.get(param_name)


def as_int_list(value: Any) -> list[int] | None:
    """Convert a value to a list of integers, or None if not possible.

    Args:
        value: The value to convert (should be a list).

    Returns:
        A list of integers, or None if conversion fails.
    """
    if not isinstance(value, list):
        return None

    result: list[int] = []
    for item in value:
        if isinstance(item, int):
            result.append(item)
        else:
            with contextlib.suppress(ValueError, TypeError):
                result.append(int(item))

    return result if result else None


def as_dict_list(value: Any) -> list[dict[str, Any]] | None:
    """Convert a value to a list of dicts, or None if not possible.

    Args:
        value: The value to convert (should be a list).

    Returns:
        A list of dicts, or None if value is not a list of dicts.
    """
    if not isinstance(value, list):
        return None

    result: list[dict[str, Any]] = []
    for item in value:
        if isinstance(item, dict):
            result.append(item)

    return result if result else None


def truncate(text: str, max_length: int) -> str:
    """Truncate a string to the specified maximum length, appending an ellipsis if truncated.

    Args:
        text: The text to truncate.
        max_length: The maximum length.

    Returns:
        The truncated string.
    """
    return text if len(text) <= max_length else text[:max_length] + "…"


# endregion

# region Base Class


class ToolCallFormatter(ABC):
    """Base class for tool call formatters that produce human-readable display strings
    for function call content items shown in the console.
    """

    @abstractmethod
    def can_format(self, call: Content) -> bool:
        """Return True if this formatter can handle the given function call.

        Args:
            call: The function call content to check.

        Returns:
            True if this formatter should be used; otherwise False.
        """
        ...

    @abstractmethod
    def format_detail(self, call: Content) -> str | None:
        """Return the detail portion of the formatted output for the given tool call,
        or None if only the tool name should be displayed.

        Args:
            call: The function call content to format.

        Returns:
            A detail string to append after the tool name, or None.
        """
        ...


# endregion

# region Concrete Formatters


class FallbackToolFormatter(ToolCallFormatter):
    """Catch-all formatter that handles any tool not matched by a more specific formatter.

    Displays a generic summary of the tool's arguments. This formatter should always be
    placed last in the formatter list.
    """

    def can_format(self, call: Content) -> bool:
        """Always returns True - this formatter matches everything."""
        return True

    def format_detail(self, call: Content) -> str | None:
        """Format arguments as generic (key: value, ...) pairs."""
        if call.arguments is None:
            return None

        # Parse arguments
        if isinstance(call.arguments, str):
            try:
                args_dict = json.loads(call.arguments)
            except (json.JSONDecodeError, TypeError):
                return None
            if not isinstance(args_dict, dict):
                return None
        elif isinstance(call.arguments, dict):
            args_dict = call.arguments
        else:
            return None

        if not args_dict:
            return None

        # Build argument list
        parts: list[str] = []
        for key, value in args_dict.items():
            if value is None:
                continue

            # Convert value to string
            if isinstance(value, bool):
                str_value = "true" if value else "false"
            elif isinstance(value, (int, float)):
                str_value = str(value)
            elif isinstance(value, str):
                str_value = value
            else:
                # Complex types - skip for now
                continue

            parts.append(f"{key}: {truncate(str_value, 40)}")

        return f"({', '.join(parts)})" if parts else None


class WebSearchToolFormatter(ToolCallFormatter):
    """Formats web_search tool calls, showing the search query."""

    def can_format(self, call: Content) -> bool:
        """Match web_search tool calls."""
        return call.name == "web_search"

    def format_detail(self, call: Content) -> str | None:
        """Extract and format the query parameter."""
        value = get_argument_value(call, "query")
        return f"({value})" if value else None


class TodoToolFormatter(ToolCallFormatter):
    """Formats todos_* tool calls with tree-view output for added items
    and structured output for complete/remove operations.
    """

    def can_format(self, call: Content) -> bool:
        """Match todos_* tool calls."""
        return call.name is not None and call.name.startswith("todos_")

    def format_detail(self, call: Content) -> str | None:
        """Format based on the specific todos operation."""
        if call.name == "todos_add":
            return self._format_add_todos(call)
        if call.name == "todos_complete":
            return self._format_complete_todos(call)
        if call.name == "todos_remove":
            return self._format_id_list(call, "ids", "Remove")
        return None

    def _format_add_todos(self, call: Content) -> str | None:
        """Format todos_add with tree view of titles."""
        todos = as_dict_list(get_argument_value(call, "todos"))
        if not todos:
            return None

        titles: list[str] = []
        for todo in todos:
            title = todo.get("title")
            if title and isinstance(title, str):
                titles.append(title)

        if not titles:
            return None

        # Build tree view
        count = len(titles)
        plural = "s" if count != 1 else ""
        lines = [f"({count} item{plural})"]
        for i, title in enumerate(titles):
            connector = "├─" if i < count - 1 else "└─"
            lines.append(f"\n   {connector} {title}")

        return "".join(lines)

    def _format_complete_todos(self, call: Content) -> str | None:
        """Format todos_complete with tree view of IDs and reasons."""
        items = as_dict_list(get_argument_value(call, "items"))
        if not items:
            return None

        entries: list[tuple[int, str | None]] = []
        for item in items:
            todo_id = item.get("id")
            if not isinstance(todo_id, int):
                continue

            reason = item.get("reason")
            reason_str = str(reason) if reason is not None and not isinstance(reason, str) else reason
            entries.append((todo_id, reason_str))

        if not entries:
            return None

        # Build tree view
        lines: list[str] = []
        for i, (todo_id, reason) in enumerate(entries):
            connector = "├─" if i < len(entries) - 1 else "└─"
            line = f"\n   {connector} Complete #{todo_id}"
            if reason:
                line += f" — {truncate(reason, 80)}"
            lines.append(line)

        return "".join(lines)

    def _format_id_list(self, call: Content, param_name: str, verb: str) -> str | None:
        """Format a list of IDs with a verb (e.g., Remove #1, Remove #2)."""
        ids = as_int_list(get_argument_value(call, param_name))
        if not ids:
            return None

        lines: list[str] = []
        for i, todo_id in enumerate(ids):
            connector = "├─" if i < len(ids) - 1 else "└─"
            lines.append(f"\n   {connector} {verb} #{todo_id}")

        return "".join(lines)


class ModeToolFormatter(ToolCallFormatter):
    """Formats mode_* tool calls, showing the target mode for set operations."""

    def can_format(self, call: Content) -> bool:
        """Match mode_* tool calls."""
        return call.name is not None and call.name.startswith("mode_")

    def format_detail(self, call: Content) -> str | None:
        """Format based on the specific mode operation."""
        if call.name == "mode_set":
            value = get_argument_value(call, "mode")
            return f"({value})" if value else None
        return None


class BackgroundAgentToolFormatter(ToolCallFormatter):
    """Formats background_agents_* tool calls with human-readable details
    for task start, continue, wait, and result retrieval operations.
    """

    def can_format(self, call: Content) -> bool:
        """Match background_agents_* tool calls."""
        return call.name is not None and call.name.startswith("background_agents_")

    def format_detail(self, call: Content) -> str | None:
        """Format based on the specific background_agents operation."""
        if call.name == "background_agents_start_task":
            return self._format_start_background_task(call)
        if call.name == "background_agents_wait_for_first_completion":
            return self._format_id_list(call, "task_ids", "Wait for")
        if call.name == "background_agents_get_task_results":
            return self._format_single_id(call, "task_id")
        if call.name == "background_agents_continue_task":
            return self._format_continue_task(call)
        if call.name == "background_agents_clear_completed_task":
            return self._format_single_id(call, "task_id")
        return None

    def _format_start_background_task(self, call: Content) -> str | None:
        """Format start_task with agent name and description."""
        agent_name = get_argument_value(call, "agent_name")
        description = get_argument_value(call, "description")

        if agent_name is None and description is None:
            return None

        lines: list[str] = []

        if agent_name is not None and description is not None:
            lines.append(f"\n   ├─ Agent: {agent_name}")
            lines.append(f'\n   └─ "{truncate(description, 80)}"')
        elif agent_name is not None:
            lines.append(f"\n   └─ Agent: {agent_name}")
        else:
            lines.append(f'\n   └─ "{truncate(description, 80)}"')  # type: ignore[arg-type]

        return "".join(lines)

    def _format_id_list(self, call: Content, param_name: str, verb: str) -> str | None:
        """Format a list of task IDs with a verb."""
        ids = as_int_list(get_argument_value(call, param_name))
        if not ids:
            return None

        lines: list[str] = []
        for i, task_id in enumerate(ids):
            connector = "├─" if i < len(ids) - 1 else "└─"
            lines.append(f"\n   {connector} {verb} #{task_id}")

        return "".join(lines)

    def _format_single_id(self, call: Content, param_name: str) -> str | None:
        """Format a single task ID in parentheses."""
        task_id = get_argument_value(call, param_name)
        if isinstance(task_id, int):
            return f"(task #{task_id})"
        return None

    def _format_continue_task(self, call: Content) -> str | None:
        """Format continue_task with task ID and optional text."""
        task_id = get_argument_value(call, "task_id")
        text = get_argument_value(call, "text")

        if not isinstance(task_id, int):
            return None

        if text:
            lines = [
                f"\n   ├─ Task #{task_id}",
                f'\n   └─ "{truncate(text, 80)}"',
            ]
            return "".join(lines)

        return f"\n   └─ Task #{task_id}"


class FileMemoryToolFormatter(ToolCallFormatter):
    """Formats file_memory_* tool calls, showing file names and search patterns
    with tree-view corners for write operations.
    """

    def can_format(self, call: Content) -> bool:
        """Match file_memory_* tool calls."""
        return call.name is not None and call.name.startswith("file_memory_")

    def format_detail(self, call: Content) -> str | None:
        """Format based on the specific file_memory operation."""
        if call.name == "file_memory_write":
            return self._format_write_file(call)
        if call.name in ("file_memory_read", "file_memory_delete", "file_memory_replace", "file_memory_replace_lines"):
            value = get_argument_value(call, "file_name")
            return f"({value})" if value else None
        if call.name == "file_memory_grep":
            return self._format_search_files(call)
        return None

    def _format_write_file(self, call: Content) -> str | None:
        """Format write_file with file name and description indicator."""
        file_name = get_argument_value(call, "file_name")
        description = get_argument_value(call, "description")

        if not file_name:
            return None

        if description:
            return f"\n   └─ {file_name} (with description)"
        return f"\n   └─ {file_name}"

    def _format_search_files(self, call: Content) -> str | None:
        """Format grep with regex pattern and optional glob pattern."""
        pattern = get_argument_value(call, "regex_pattern")
        glob_pattern = get_argument_value(call, "glob_pattern")

        if not pattern:
            return None

        if glob_pattern:
            return f"(/{pattern}/ in {glob_pattern})"
        return f"(/{pattern}/)"


# endregion

# region Public API Functions


def format_tool_call(formatters: list[ToolCallFormatter], call: Content) -> str:
    """Format a tool call using the first matching formatter from the provided list.

    Returns "{toolName} {detail}" when a formatter produces detail,
    or just "{toolName}" otherwise.

    Args:
        formatters: List of formatters to try in order.
        call: The function call content to format.

    Returns:
        Formatted string representation of the tool call.
    """
    for formatter in formatters:
        if formatter.can_format(call):
            detail = formatter.format_detail(call)
            tool_name = call.name or "Unknown"
            return f"{tool_name} {detail}" if detail is not None else tool_name

    return call.name or "Unknown"


def build_default_formatters() -> list[ToolCallFormatter]:
    """Create the default list of tool call formatters.

    The FallbackToolFormatter is always last. Users can call this function
    and combine the result with their own formatters.

    Returns:
        A list of all built-in tool call formatters.
    """
    return [
        TodoToolFormatter(),
        ModeToolFormatter(),
        BackgroundAgentToolFormatter(),
        FileMemoryToolFormatter(),
        WebSearchToolFormatter(),
        FallbackToolFormatter(),
    ]


# endregion
