"""
Tool search orchestration for persistent agents.

Provides a unified `search_tools` function that queries the LLM once across
both MCP-discovered tools and builtin tools, then enables any selected tools.
"""

import json
import logging
import re
from functools import partial
from typing import Any, Callable, Dict, Iterable, List, Optional

import litellm  # re-exported for tests that patch LiteLLM directly
from django.conf import settings
from django.db.models import F
from django.urls import NoReverseMatch, reverse
from django.utils import timezone
from litellm import drop_params
from opentelemetry import trace

from ...models import MCPServerConfig, PersistentAgent, PersistentAgentCompletion, PersistentAgentSkill
from ...services.pipedream_apps import (
    PipedreamCatalogError,
    PipedreamCatalogService,
    enable_pipedream_apps_for_agent,
    get_effective_pipedream_app_slugs_for_agent,
)
from ...services.tool_blacklist import get_agent_tool_blacklist
from ...services.tool_settings import get_tool_settings_for_owner
from ...evals.execution import get_current_eval_routing_profile
from ..system_skills import shortlist_system_skills
from ..system_skills.service import enable_system_skills
from ..core.llm_config import LLMNotConfiguredError, get_llm_config_with_failover
from ..core.llm_utils import run_completion
from ..core.token_usage import log_agent_completion, set_usage_span_attributes
from .global_skills import enable_global_skills, get_compatible_global_skills
from .eval_synthetic_tools import get_available_eval_synthetic_tool_catalog
from .mcp_manager import get_mcp_manager
from .skill_utils import format_skill_secret_requirement, normalize_skill_tool_ids
from .sqlite_skills import get_latest_skill_versions
from .tool_manager import (
    enable_tools,
    CREATE_IMAGE_TOOL_NAME,
    HTTP_REQUEST_TOOL_NAME,
    get_available_builtin_tool_entries,
    get_available_custom_tool_entries,
    get_enabled_tool_limit,
)
from .autotool_heuristics import find_matching_tools

logger = logging.getLogger(__name__)
tracer = trace.get_tracer("gobii.utils")

ToolSearchResult = Dict[str, Any]


def _has_active_pipedream_runtime() -> bool:
    return MCPServerConfig.objects.filter(
        scope=MCPServerConfig.Scope.PLATFORM,
        name="pipedream",
        is_active=True,
    ).exists()


def _build_console_url(route_name: str) -> str:
    try:
        path = reverse(route_name)
    except NoReverseMatch:
        logger.debug("search_tools: failed to reverse route %s", route_name, exc_info=True)
        return ""

    base_url = (getattr(settings, "PUBLIC_SITE_URL", "") or "").strip().rstrip("/")
    if base_url:
        return f"{base_url}{path}"
    return path


def _strip_description(text: str, limit: int = 180) -> str:
    if not text:
        return ""
    cleaned = re.sub(r"\[([^\]]+)\]\([^\)]+\)", r"\1", text)
    cleaned = re.sub(r"https?://\S+", "", cleaned)
    cleaned = re.sub(r"\s+", " ", cleaned).strip()
    return cleaned[:limit].rstrip() + ("…" if len(cleaned) > limit else "")


def _summarize_parameters(schema: Dict[str, Any], limit: int = 6) -> str:
    try:
        if not isinstance(schema, dict):
            return ""
        props = schema.get("properties") or {}
        if not isinstance(props, dict) or not props:
            return ""
        required = set(schema.get("required") or [])
        items: List[str] = []
        for idx, (key, value) in enumerate(props.items()):
            if idx >= limit:
                items.append(f"+{len(props) - limit} more")
                break
            param_type = value.get("type") if isinstance(value, dict) else None
            param_type = param_type if isinstance(param_type, str) else "any"
            suffix = "*" if key in required else ""
            items.append(f"{key}{suffix}:{param_type}")
        return ", ".join(items)
    except Exception:  # pragma: no cover - defensive safety
        return ""


def _tool_attr(tool: Any, attr: str, default: Any = None) -> Any:
    if hasattr(tool, attr):
        return getattr(tool, attr)
    if isinstance(tool, dict):
        return tool.get(attr, default)
    return default


def _fallback_builtin_selection(
    query: str,
    content_text: str,
    available_names: set[str],
) -> list[str]:
    """
    Heuristically select tools when the LLM response does not call enable_tools.

    Uses keyword matching from autotool_heuristics for MCP tools (LinkedIn, Crunchbase, etc.)
    and basic keyword matching for builtins like http_request.
    """
    text = f"{query} {content_text}".lower()
    candidates: list[str] = []

    # Use autotool heuristics to find matching MCP tools (linkedin, crunchbase, etc.)
    heuristic_matches = find_matching_tools(text)
    for tool_name in heuristic_matches:
        if tool_name in available_names and tool_name not in candidates:
            candidates.append(tool_name)

    # Also check for API/http keywords for http_request
    wants_api = any(keyword in text for keyword in ["api", "http", "https", "request", "fetch", "endpoint", "json"])
    if wants_api and HTTP_REQUEST_TOOL_NAME in available_names:
        if HTTP_REQUEST_TOOL_NAME not in candidates:
            candidates.append(HTTP_REQUEST_TOOL_NAME)

    wants_image = any(
        keyword in text
        for keyword in [
            "generate image",
            "generate an image",
            "image generation",
            "create image",
            "make image",
            "render image",
            "edit image",
            "modify image",
            "transform image",
            "image to image",
            "style transfer",
            "illustration",
            "illustrate",
            "create logo",
            "logo design",
            "poster design",
            "thumbnail design",
            "concept art",
            "artwork",
        ]
    )
    if wants_image and CREATE_IMAGE_TOOL_NAME in available_names:
        if CREATE_IMAGE_TOOL_NAME not in candidates:
            candidates.append(CREATE_IMAGE_TOOL_NAME)

    if candidates:
        logger.info(
            "search_tools: heuristic fallback matched %d tools from query '%s': %s",
            len(candidates),
            query[:80],
            ", ".join(candidates[:5]) + ("..." if len(candidates) > 5 else ""),
        )

    return candidates


def _normalize_named_selection_text(value: str) -> str:
    return re.sub(r"[^a-z0-9]+", " ", value.lower()).strip()


def _fallback_named_selection(query: str, available_names: set[str]) -> list[str]:
    """
    Select explicitly named skills when the inner LLM returns no tool calls.

    This keeps exact skill searches from falling through to keyword-based
    builtin matching when a skill name contains words like "http" or "api".
    """
    query_key = _normalize_named_selection_text(query or "")
    if not query_key:
        return []

    padded_query = f" {query_key} "
    selected: list[str] = []
    for name in sorted(available_names, key=lambda item: (-len(item), item)):
        name_key = _normalize_named_selection_text(name)
        if not name_key:
            continue
        if query_key == name_key or f" {name_key} " in padded_query:
            selected.append(name)
    return selected


def _find_tool_by_suffix(
    available_names: Iterable[str],
    suffix: str,
    *,
    exclude_suffixes: Optional[Iterable[str]] = None,
) -> Optional[str]:
    exclude_suffixes = set(exclude_suffixes or [])
    candidates = []
    for name in available_names:
        if name == suffix or name.endswith(f"_{suffix}"):
            if any(name.endswith(f"_{exclude}") or name == exclude for exclude in exclude_suffixes):
                continue
            candidates.append(name)
    if not candidates:
        return None
    return sorted(candidates, key=lambda v: (len(v), v))[0]


def _build_tool_examples(available_names: set[str]) -> str:
    def example(query_text: str, suffixes: List[str]) -> Optional[str]:
        tools: List[str] = []
        for suffix in suffixes:
            tool_name = _find_tool_by_suffix(
                available_names,
                suffix,
                exclude_suffixes=["search_engine_batch"] if suffix == "search_engine" else None,
            )
            if tool_name:
                tools.append(tool_name)
        if len(tools) < 2:
            return None
        tool_list = ", ".join(f"`{name}`" for name in tools)
        return f"**Query:** \"{query_text}\"\n**Tools:** {tool_list}\n"

    examples = [
        example(
            "Research Stripe as a company",
            [
                "search_engine",
                "scrape_as_markdown",
                "web_data_linkedin_company_profile",
                "web_data_crunchbase_company",
                "web_data_yahoo_finance_business",
            ],
        ),
        example(
            "Find info about Elon Musk",
            [
                "search_engine",
                "scrape_as_markdown",
                "web_data_linkedin_person_profile",
                "web_data_x_posts",
                "web_data_instagram_profiles",
            ],
        ),
        example(
            "Analyze sentiment on Nike products",
            [
                "search_engine",
                "scrape_as_markdown",
                "web_data_amazon_product",
                "web_data_amazon_product_reviews",
                "web_data_reddit_posts",
                "web_data_x_posts",
            ],
        ),
        example(
            "Job openings at Google",
            [
                "search_engine",
                "scrape_as_markdown",
                "web_data_linkedin_job_listings",
                "web_data_linkedin_company_profile",
            ],
        ),
        example(
            "Bitcoin price and trends",
            [
                "search_engine",
                "scrape_as_markdown",
                "web_data_yahoo_finance_business",
                "web_data_reddit_posts",
            ],
        ),
        example(
            "GitHub repository file details",
            [
                "web_data_github_repository_file",
                "search_engine",
            ],
        ),
    ]
    example_text = "\n".join(item.strip() for item in examples if item)
    return example_text


def _build_app_lines(
    app_catalog: Iterable[Any],
    *,
    enabled_app_slugs: Optional[Iterable[str]] = None,
) -> list[str]:
    enabled_set = set(enabled_app_slugs or [])
    lines: list[str] = []
    for app in app_catalog:
        slug = _tool_attr(app, "slug")
        name = _tool_attr(app, "name")
        if not isinstance(slug, str) or not slug:
            continue
        label = name if isinstance(name, str) and name.strip() else slug
        status = "enabled" if slug in enabled_set else "not enabled"
        lines.append(f"- {slug} | {label} [{status}]")
    return lines


def _build_global_skill_lines(global_skills: Iterable[Any]) -> list[str]:
    lines: list[str] = []
    for skill in global_skills:
        name = _tool_attr(skill, "name")
        if not isinstance(name, str) or not name:
            continue
        description = _strip_description(_tool_attr(skill, "description", "") or "")
        instructions = _strip_description(_tool_attr(skill, "instructions", "") or "")
        if hasattr(skill, "get_effective_tool_ids"):
            normalized_tools = list(skill.get_effective_tool_ids())
        else:
            normalized_tools = list(normalize_skill_tool_ids(_tool_attr(skill, "tools", []) or []))
        use_when = description or instructions
        tool_text = ", ".join(normalized_tools) if normalized_tools else "(none)"
        raw_secrets = _tool_attr(skill, "secrets", []) or []
        secret_text = ", ".join(
            format_skill_secret_requirement(secret)
            for secret in raw_secrets
            if isinstance(secret, dict)
        ) or "(none)"
        line = f"- {name}"
        if use_when:
            line += f" | use when: {use_when}"
        line += f" | enables: {tool_text}"
        line += f" | secrets: {secret_text}"
        lines.append(line)
    return lines


def _build_agent_skill_lines(agent_skills: Iterable[PersistentAgentSkill]) -> list[str]:
    lines: list[str] = []
    for skill in agent_skills:
        name = (skill.name or "").strip()
        if not name:
            continue
        description = _strip_description(skill.description or "")
        instructions = _strip_description(skill.instructions or "")
        normalized_tools = list(normalize_skill_tool_ids(skill.tools))
        use_when = description or instructions
        tool_text = ", ".join(normalized_tools) if normalized_tools else "(none)"
        raw_secrets = skill.secrets or []
        secret_text = ", ".join(
            format_skill_secret_requirement(secret)
            for secret in raw_secrets
            if isinstance(secret, dict)
        ) or "(none)"
        line = f"- {name}"
        if use_when:
            line += f" | use when: {use_when}"
        line += f" | enables: {tool_text}"
        line += f" | secrets: {secret_text}"
        lines.append(line)
    return lines


def _filter_agent_skills_for_tool_blacklist(
    agent_skills: Iterable[PersistentAgentSkill],
    blacklisted_tools: set[str],
) -> list[PersistentAgentSkill]:
    if not blacklisted_tools:
        return list(agent_skills)
    return [
        skill for skill in agent_skills
        if not (set(normalize_skill_tool_ids(skill.tools)) & blacklisted_tools)
    ]


def _build_system_skill_lines(system_skills: Iterable[Any]) -> list[str]:
    lines: list[str] = []
    for skill in system_skills:
        skill_key = _tool_attr(skill, "skill_key")
        if not isinstance(skill_key, str) or not skill_key:
            continue
        search_summary = _strip_description(_tool_attr(skill, "search_summary", "") or "")
        enables = list(_tool_attr(skill, "enables", ()) or ())
        use_when = list(_tool_attr(skill, "use_when", ()) or ())
        tool_names = list(_tool_attr(skill, "tool_names", ()) or ())
        enable_text = ", ".join(str(item).strip() for item in enables if str(item).strip())
        use_when_text = ", ".join(str(item).strip() for item in use_when if str(item).strip())
        tool_text = ", ".join(tool_names) if tool_names else "(none)"
        line = f"- {skill_key}"
        if search_summary:
            line += f": {search_summary}"
        if use_when_text:
            line += f" | use when: {use_when_text}"
        if enable_text:
            line += f" | enables: {enable_text}"
        line += f" | tools: {tool_text}"
        lines.append(line)
    return lines


def _build_search_tool_definitions(
    *,
    max_items: int,
    include_agent_skills: bool,
    include_global_skills: bool,
    include_system_skills: bool,
    include_app_enablement: bool,
) -> list[dict[str, Any]]:
    tool_defs: list[dict[str, Any]] = [
        {
            "type": "function",
            "function": {
                "name": "enable_tools",
                "description": (
                    "Enable exact tools from the catalog; optionally suggest verified external resources."
                ),
                "parameters": {
                    "type": "object",
                    "properties": {
                        "tool_names": {
                            "type": "array",
                            "items": {"type": "string"},
                            "minItems": 1,
                            "maxItems": max_items,
                            "description": "List of full tool names to enable",
                        },
                        "external_resources": {
                            "type": "array",
                            "items": {
                                "type": "object",
                                "properties": {
                                    "name": {"type": "string", "description": "Resource name"},
                                    "description": {"type": "string", "description": "Brief description"},
                                    "url": {"type": "string", "description": "Full URL"},
                                },
                                "required": ["name", "description", "url"],
                            },
                            "maxItems": 5,
                            "description": "Public APIs, websites, or datasets with verified URLs",
                        },
                    },
                    "required": ["tool_names"],
                },
            },
        }
    ]
    if include_agent_skills:
        tool_defs.append(
            {
                "type": "function",
                "function": {
                    "name": "enable_agent_skills",
                    "description": (
                        "Refresh exact saved agent skills and enable their required tools."
                    ),
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "skill_names": {
                                "type": "array",
                                "items": {"type": "string"},
                                "minItems": 1,
                                "maxItems": 20,
                                "description": "List of exact saved agent skill names to refresh",
                            },
                        },
                        "required": ["skill_names"],
                    },
                },
            }
        )
    if include_global_skills:
        tool_defs.append(
            {
                "type": "function",
                "function": {
                    "name": "enable_global_skills",
                    "description": (
                        "Import exact global skills into the agent's local skill history."
                    ),
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "skill_names": {
                                "type": "array",
                                "items": {"type": "string"},
                                "minItems": 1,
                                "maxItems": 20,
                                "description": "List of exact global skill names to enable",
                            },
                        },
                        "required": ["skill_names"],
                    },
                },
            }
        )
    if include_system_skills:
        tool_defs.append(
            {
                "type": "function",
                "function": {
                    "name": "enable_system_skills",
                    "description": (
                        "Enable exact code-defined system skills and their hidden tools."
                    ),
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "skill_keys": {
                                "type": "array",
                                "items": {"type": "string"},
                                "minItems": 1,
                                "maxItems": 20,
                                "description": "List of exact system skill keys to enable",
                            },
                        },
                        "required": ["skill_keys"],
                    },
                },
            }
        )
    if include_app_enablement:
        tool_defs.append(
            {
                "type": "function",
                "function": {
                    "name": "enable_apps",
                    "description": (
                        "Enable exact Pipedream app slugs for future tool discovery."
                    ),
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "app_slugs": {
                                "type": "array",
                                "items": {"type": "string"},
                                "minItems": 1,
                                "maxItems": 20,
                                "description": "List of exact Pipedream app slugs to enable",
                            },
                        },
                        "required": ["app_slugs"],
                    },
                },
            }
        )
    return tool_defs


def _parse_search_tool_calls(tool_calls: Iterable[Any], *, provider_name: str) -> dict[str, list[Any]]:
    requested_tools: list[str] = []
    requested_agent_skills: list[str] = []
    requested_skills: list[str] = []
    requested_system_skills: list[str] = []
    requested_apps: list[str] = []
    external_resources: list[dict[str, str]] = []

    for tool_call in tool_calls:
        try:
            if not tool_call:
                continue
            function_block = getattr(tool_call, "function", None) or tool_call.get("function")
            if not function_block:
                continue
            function_name = getattr(function_block, "name", None) or function_block.get("name")
            raw_args = getattr(function_block, "arguments", None) or function_block.get("arguments") or "{}"
            arguments = json.loads(raw_args)
            if function_name == "enable_tools":
                names = arguments.get("tool_names") or []
                if isinstance(names, list):
                    for name in names:
                        if isinstance(name, str) and name not in requested_tools:
                            requested_tools.append(name)
                resources = arguments.get("external_resources") or []
                if isinstance(resources, list):
                    for res in resources:
                        if isinstance(res, dict) and res.get("name") and res.get("url"):
                            url = res.get("url", "")
                            if url.startswith("http://") or url.startswith("https://"):
                                external_resources.append(
                                    {
                                        "name": str(res.get("name", ""))[:100],
                                        "description": str(res.get("description", ""))[:200],
                                        "url": url[:500],
                                    }
                                )
            elif function_name == "enable_global_skills":
                skill_names = arguments.get("skill_names") or []
                if isinstance(skill_names, list):
                    for skill_name in skill_names:
                        if isinstance(skill_name, str) and skill_name not in requested_skills:
                            requested_skills.append(skill_name)
            elif function_name == "enable_agent_skills":
                skill_names = arguments.get("skill_names") or []
                if isinstance(skill_names, list):
                    for skill_name in skill_names:
                        if isinstance(skill_name, str) and skill_name not in requested_agent_skills:
                            requested_agent_skills.append(skill_name)
            elif function_name == "enable_system_skills":
                skill_keys = arguments.get("skill_keys") or []
                if isinstance(skill_keys, list):
                    for skill_key in skill_keys:
                        if isinstance(skill_key, str) and skill_key not in requested_system_skills:
                            requested_system_skills.append(skill_key)
            elif function_name == "enable_apps":
                app_slugs = arguments.get("app_slugs") or []
                if isinstance(app_slugs, list):
                    for app_slug in app_slugs:
                        if isinstance(app_slug, str) and app_slug not in requested_apps:
                            requested_apps.append(app_slug)
        except Exception:  # pragma: no cover - defensive parsing
            logger.exception("search_tools.%s: failed to parse tool call; skipping", provider_name)

    return {
        "tools": requested_tools,
        "agent_skills": requested_agent_skills,
        "skills": requested_skills,
        "system_skills": requested_system_skills,
        "apps": requested_apps,
        "external_resources": external_resources,
    }


def _normalize_requested_selection(
    requested: list[str],
    available_names: set[str],
    *,
    provider_name: str,
    item_label: str,
) -> list[str]:
    valid_requested = [name for name in requested if name in available_names]
    invalid_requested = [name for name in requested if name not in available_names]
    if invalid_requested:
        logger.info(
            "search_tools.%s: ignoring invalid %s: %s",
            provider_name,
            item_label,
            ", ".join(invalid_requested[:10]) + ("..." if len(invalid_requested) > 10 else ""),
        )
    return valid_requested


def _enable_agent_skills(
    agent: PersistentAgent,
    skill_names: list[str],
    *,
    available_skills: Optional[Iterable[PersistentAgentSkill]] = None,
) -> dict[str, Any]:
    catalog = {
        skill.name: skill
        for skill in (list(available_skills) if available_skills is not None else get_latest_skill_versions(agent))
        if skill.name
    }
    requested: list[str] = []
    for name in skill_names:
        normalized = str(name or "").strip()
        if normalized and normalized not in requested:
            requested.append(normalized)

    invalid = [name for name in requested if name not in catalog]
    selected = [catalog[name] for name in requested if name in catalog]
    if not selected:
        return {
            "status": "success",
            "enabled": [],
            "already_enabled": [],
            "invalid": invalid,
            "evicted": [],
        }

    used_at = timezone.now()
    PersistentAgentSkill.objects.filter(id__in=[skill.id for skill in selected]).update(
        last_used_at=used_at,
        usage_count=F("usage_count") + 1,
    )

    required_tools = sorted(
        {
            tool_id
            for skill in selected
            for tool_id in normalize_skill_tool_ids(skill.tools)
        }
    )
    enabled_tools_result: Optional[dict[str, Any]] = None
    if required_tools:
        enabled_tools_result = enable_tools(agent, required_tools)

    result = {
        "status": "success",
        "enabled": [skill.name for skill in selected],
        "already_enabled": [],
        "invalid": invalid,
        "evicted": [],
    }
    if enabled_tools_result:
        result["tools"] = enabled_tools_result
        if enabled_tools_result.get("status") == "success":
            result["evicted"] = list(enabled_tools_result.get("evicted", []))
    return result


def _section_payload(
    *,
    enabled: Optional[list[str]] = None,
    already_enabled: Optional[list[str]] = None,
    evicted: Optional[list[str]] = None,
    invalid: Optional[list[str]] = None,
    conflicts: Optional[list[str]] = None,
    effective_apps: Optional[list[str]] = None,
) -> dict[str, Any]:
    payload: dict[str, Any] = {
        "enabled": list(enabled or []),
        "already_enabled": list(already_enabled or []),
    }
    if evicted is not None:
        payload["evicted"] = list(evicted)
    if invalid is not None:
        payload["invalid"] = list(invalid)
    if conflicts is not None:
        payload["conflicts"] = list(conflicts)
    if effective_apps is not None:
        payload["effective_apps"] = list(effective_apps)
    return payload


def _append_section_summary(
    message_lines: list[str],
    result: Optional[dict[str, Any]],
    *,
    enabled_label: str,
    already_enabled_label: str,
    evicted_label: Optional[str] = None,
    invalid_label: Optional[str] = None,
    conflicts_label: Optional[str] = None,
    failed_label: Optional[str] = None,
) -> None:
    if not result or result.get("status") != "success":
        return

    summary: list[str] = []
    if result.get("enabled"):
        summary.append(f"{enabled_label}: {', '.join(result['enabled'])}")
    if result.get("already_enabled"):
        summary.append(f"{already_enabled_label}: {', '.join(result['already_enabled'])}")
    if evicted_label and result.get("evicted"):
        summary.append(f"{evicted_label}: {', '.join(result['evicted'])}")
    if invalid_label and result.get("invalid"):
        summary.append(f"{invalid_label}: {', '.join(result['invalid'])}")
    if conflicts_label and result.get("conflicts"):
        summary.append(f"{conflicts_label}: {', '.join(result['conflicts'])}")
    if failed_label and result.get("failed"):
        summary.append(f"{failed_label}: {', '.join(result['failed'])}")
    if summary:
        message_lines.append("; ".join(summary))


def _search_with_llm(
    agent: PersistentAgent,
    query: str,
    provider_name: str,
    catalog: Iterable[Any],
    enable_callback: Callable[[PersistentAgent, List[str]], Dict[str, Any]],
    empty_message: str,
    *,
    agent_skill_catalog: Optional[Iterable[PersistentAgentSkill]] = None,
    global_skill_catalog: Optional[Iterable[Any]] = None,
    system_skill_catalog: Optional[Iterable[Any]] = None,
    enable_apps_callback: Optional[Callable[[PersistentAgent, List[str]], Dict[str, Any]]] = None,
    pipedream_app_catalog: Optional[Iterable[Any]] = None,
    enabled_app_slugs: Optional[Iterable[str]] = None,
    auto_enable_apps: bool = True,
) -> ToolSearchResult:
    tools = list(catalog)
    agent_skills = list(agent_skill_catalog or [])
    global_skills = list(global_skill_catalog or [])
    system_skills = list(system_skill_catalog or [])
    app_catalog = list(pipedream_app_catalog or [])
    logger.info(
        "search_tools.%s: %d tools available, %d agent skills available, %d global skills available, %d system skills available, %d pipedream apps available",
        provider_name,
        len(tools),
        len(agent_skills),
        len(global_skills),
        len(system_skills),
        len(app_catalog),
    )

    if not tools and not agent_skills and not global_skills and not system_skills and not app_catalog:
        return {"status": "success", "tools": [], "message": empty_message}
    available_names = {
        _tool_attr(tool, "full_name") or _tool_attr(tool, "name")
        for tool in tools
    }
    available_global_skill_names = {
        _tool_attr(skill, "name")
        for skill in global_skills
        if isinstance(_tool_attr(skill, "name"), str)
    }
    available_agent_skill_names = {
        skill.name
        for skill in agent_skills
        if isinstance(skill.name, str)
    }
    available_system_skill_keys = {
        _tool_attr(skill, "skill_key")
        for skill in system_skills
        if isinstance(_tool_attr(skill, "skill_key"), str)
    }

    tool_lines: List[str] = []
    for tool in tools:
        full_name = _tool_attr(tool, "full_name") or _tool_attr(tool, "name")
        description = _tool_attr(tool, "description", "")
        parameters = _tool_attr(tool, "parameters", {})
        line = f"- {full_name}"
        desc = _strip_description(description or "")
        if desc:
            line += f": {desc}"
        params_summary = _summarize_parameters(parameters or {})
        if params_summary:
            line += f" | params: {params_summary}"
        tool_lines.append(line)

    try:
        preview = "\n".join(tool_lines[:5])
        logger.info(
            "search_tools.%s: compact catalog prepared with %d entries; first few:\n%s",
            provider_name,
            len(tool_lines),
            preview,
        )
        if len(tool_lines) > 5:
            logger.info(
                "search_tools.%s: (truncated catalog log; total entries=%d)",
                provider_name,
                len(tool_lines),
            )
    except Exception:  # pragma: no cover - defensive logging
        logger.exception("search_tools.%s: failed to log compact catalog preview", provider_name)

    agent_skill_lines = _build_agent_skill_lines(agent_skills)
    global_skill_lines = _build_global_skill_lines(global_skills)
    system_skill_lines = _build_system_skill_lines(system_skills)
    app_lines = _build_app_lines(app_catalog, enabled_app_slugs=enabled_app_slugs)
    enable_apps_manually_url = _build_console_url("console-mcp-servers")
    manual_app_guidance = (
        f'If a needed Pipedream app is not enabled yet and you require it, tell the user to go to "Add Apps" here: '
        f"{enable_apps_manually_url} and search for the exact app slug.\n"
        "Do this sparingly and only if you truly require the integration. "
        "You may already have the tools you need for integration via http_request or other methods.\n"
    )

    examples_text = _build_tool_examples(available_names)
    examples_block = f"## Examples\n\n{examples_text}\n\n" if examples_text else ""
    selection_example = (
        "\nExample placeholders: tool_names: [\"<TOOL>\"]; "
        "enable_agent_skills.skill_names: [\"<SAVED_SKILL>\"]; "
        "skill_names: [\"<GLOBAL_SKILL>\"]; skill_keys: [\"<SYSTEM_SKILL>\"]\n"
    )
    image_generation_rules = ""
    if CREATE_IMAGE_TOOL_NAME in available_names:
        image_generation_rules = (
            f"- If the user asks to generate or design a NEW image asset, include `{CREATE_IMAGE_TOOL_NAME}`.\n"
            f"- If the user asks to edit, transform, restyle, or preserve details from an existing image, include `{CREATE_IMAGE_TOOL_NAME}`.\n"
            f"- For edit/transform requests, plan to pass `source_images` so the model can preserve identity, logos, text, or layout.\n"
            f"- Do not include `{CREATE_IMAGE_TOOL_NAME}` for image analysis, OCR, or extracting information from existing images.\n"
        )

    system_prompt = (
        "Select helpful tools and skills. Be inclusive, but copy exact names/slugs from the lists; never invent them.\n"
        "If nothing matches, call nothing.\n\n"
        f"{examples_block}"
        "## Format\n"
        "Call enable_tools with exact full names from Available tools.\n"
        "## Rules\n"
        "- Only include tools that appear in Available tools.\n"
        f"{image_generation_rules}"
        "- external_resources: include direct API endpoints when you know them\n"
        "- Format: Name | Brief description | Full URL"
    )
    system_prompt += (
        "\n- Think in small composable primitives. If the user needs recurring monitoring, comparisons over time, "
        "or anomaly detection, prefer tools or skills that sync data directly into SQLite. Then use sqlite_batch "
        "to query the synced tables instead of shuttling rows through context.\n"
    )
    if global_skill_lines:
        system_prompt += (
            "\n- Global skills: Treat global skills as capability bundles. "
            "Call enable_global_skills with exact Available global skill names when `use when`/`enables` match.\n"
        )
    if agent_skill_lines:
        system_prompt += (
            "\n- Agent skills: Treat agent skills as this agent's saved workflows. "
            "Call enable_agent_skills with exact Available agent skill names when `use when`/`enables` match.\n"
        )
    if system_skill_lines:
        system_prompt += (
            "\n- System skills: Treat system skills as capability bundles. "
            "Call enable_system_skills with exact Available system skill keys when `use when`/`enables` match.\n"
        )
    if app_lines:
        if auto_enable_apps and enable_apps_callback is not None:
            system_prompt += (
                "\n- Pipedream apps: call enable_apps with exact Available app slugs only when a needed app is missing. "
                "Do not combine enable_apps with tool/skill calls; after enabling apps, stop and search again for tools.\n"
                f"{selection_example}"
                "app_slugs: [\"<APP_SLUG>\"]\n"
            )
        else:
            system_prompt += (
                "\n- Automatic Pipedream app enablement is disabled. Do not call enable_apps.\n"
                f"{manual_app_guidance}"
                "Enable only tools/skills already listed.\n"
                f"{selection_example}"
            )
    else:
        system_prompt += selection_example
    user_prompt = f"Query: {query}\n\nAvailable tools:\n" + "\n".join(tool_lines)
    if agent_skill_lines:
        user_prompt += "\n\nAvailable agent skills:\n" + "\n".join(agent_skill_lines)
    if global_skill_lines:
        user_prompt += "\n\nAvailable global skills:\n" + "\n".join(global_skill_lines)
    if system_skill_lines:
        user_prompt += "\n\nAvailable system skills:\n" + "\n".join(system_skill_lines)
    if app_lines:
        user_prompt += (
            "\n\nAvailable Pipedream apps:\n"
            + "\n".join(app_lines)
            + "\n\nUse ONLY names/slugs from the lists above."
        )
        if auto_enable_apps and enable_apps_callback is not None:
            user_prompt += " If none match, call no enable function."
        else:
            user_prompt += (
                f' If a required app is missing, sparingly tell the user to add the exact app slug at "Add Apps": {enable_apps_manually_url}.'
            )
            user_prompt += " If none match, call no tool/skill enable function."
    else:
        user_prompt += "\n\nUse ONLY tool names, saved skill names, skill names, and skill keys from the lists above."
        user_prompt += " If none match, call no tool/skill enable function."

    try:
        failover_configs = get_llm_config_with_failover(
            agent=agent,
            routing_profile=get_current_eval_routing_profile(),
        )
        last_exc: Optional[Exception] = None
        for idx, (provider, model, params) in enumerate(failover_configs):
            try:
                logger.info(
                    "search_tools.%s: invoking provider %s/%s: provider=%s model=%s",
                    provider_name,
                    idx + 1,
                    len(failover_configs),
                    provider,
                    model,
                )
                max_items = get_enabled_tool_limit(agent)

                tool_defs = _build_search_tool_definitions(
                    max_items=max_items,
                    include_agent_skills=bool(agent_skill_lines),
                    include_global_skills=bool(global_skill_lines),
                    include_system_skills=bool(system_skill_lines),
                    include_app_enablement=bool(app_lines and auto_enable_apps and enable_apps_callback is not None),
                )

                run_kwargs: Dict[str, Any] = {}
                safety_value = getattr(agent.user, "id", None) if agent and agent.user else None
                if (
                    safety_value is not None
                    and isinstance(provider, str)
                    and provider.lower().startswith("openai")
                ):
                    run_kwargs["safety_identifier"] = str(safety_value)

                response = run_completion(
                    model=model,
                    messages=[
                        {"role": "system", "content": system_prompt},
                        {"role": "user", "content": user_prompt},
                    ],
                    params=params,
                    tools=tool_defs,
                    drop_params=True,
                    **run_kwargs,
                )

                token_usage, usage = log_agent_completion(
                    agent,
                    completion_type=PersistentAgentCompletion.CompletionType.TOOL_SEARCH,
                    response=response,
                    model=model,
                    provider=provider,
                )
                set_usage_span_attributes(trace.get_current_span(), usage)

                message = response.choices[0].message
                content_text = getattr(message, "content", None) or ""

                parsed_calls = _parse_search_tool_calls(
                    getattr(message, "tool_calls", None) or [],
                    provider_name=provider_name,
                )
                requested = _normalize_requested_selection(
                    parsed_calls["tools"],
                    available_names,
                    provider_name=provider_name,
                    item_label="tool names",
                )
                requested_agent_skills = _normalize_requested_selection(
                    parsed_calls["agent_skills"],
                    available_agent_skill_names,
                    provider_name=provider_name,
                    item_label="agent skill names",
                )
                requested_skills = _normalize_requested_selection(
                    parsed_calls["skills"],
                    available_global_skill_names,
                    provider_name=provider_name,
                    item_label="global skill names",
                )
                requested_system_skills = _normalize_requested_selection(
                    parsed_calls["system_skills"],
                    available_system_skill_keys,
                    provider_name=provider_name,
                    item_label="system skill keys",
                )
                requested_apps = parsed_calls["apps"]
                external_resources = parsed_calls["external_resources"]
                enabled_apps_result = None
                if requested_apps and enable_apps_callback is not None:
                    try:
                        enabled_apps_result = enable_apps_callback(agent, requested_apps)
                    except Exception as err:  # pragma: no cover - defensive enabling
                        logger.error("search_tools.%s: enable_apps failed: %s", provider_name, err)

                if enabled_apps_result and enabled_apps_result.get("status") == "success":
                    message_lines: List[str] = []
                    if content_text:
                        message_lines.append(content_text.strip())
                    summary: List[str] = []
                    if enabled_apps_result.get("enabled"):
                        summary.append(f"Enabled apps: {', '.join(enabled_apps_result['enabled'])}")
                    if enabled_apps_result.get("already_enabled"):
                        summary.append(f"Already enabled apps: {', '.join(enabled_apps_result['already_enabled'])}")
                    if enabled_apps_result.get("invalid"):
                        summary.append(f"Invalid apps: {', '.join(enabled_apps_result['invalid'])}")
                    if summary:
                        message_lines.append("; ".join(summary))
                    if enabled_apps_result.get("enabled") or enabled_apps_result.get("already_enabled"):
                        message_lines.append(
                            "Pipedream apps are ready. Run search_tools again to discover and enable the specific tools for those apps."
                        )
                    else:
                        message_lines.append("No Pipedream apps were enabled. Search again with one of the listed app slugs.")
                    response_payload: ToolSearchResult = {
                        "status": "success",
                        "message": "\n".join([line for line in message_lines if line]) or "",
                        "apps": _section_payload(
                            enabled=enabled_apps_result.get("enabled", []),
                            already_enabled=enabled_apps_result.get("already_enabled", []),
                            invalid=enabled_apps_result.get("invalid", []),
                            effective_apps=enabled_apps_result.get("effective_apps", []),
                        ),
                    }
                    return response_payload

                enabled_global_skills_result = None
                if requested_skills:
                    try:
                        enabled_global_skills_result = enable_global_skills(
                            agent,
                            requested_skills,
                            available_skills=global_skills,
                        )
                    except Exception as err:  # pragma: no cover - defensive enabling
                        logger.error("search_tools.%s: enable_global_skills failed: %s", provider_name, err)

                enabled_agent_skills_result = None
                if requested_agent_skills:
                    try:
                        enabled_agent_skills_result = _enable_agent_skills(
                            agent,
                            requested_agent_skills,
                            available_skills=agent_skills,
                        )
                    except Exception as err:  # pragma: no cover - defensive enabling
                        logger.error("search_tools.%s: enable_agent_skills failed: %s", provider_name, err)

                enabled_system_skills_result = None
                if requested_system_skills:
                    try:
                        enabled_system_skills_result = enable_system_skills(
                            agent,
                            requested_system_skills,
                            available_skills=system_skills,
                        )
                    except Exception as err:  # pragma: no cover - defensive enabling
                        logger.error("search_tools.%s: enable_system_skills failed: %s", provider_name, err)

                enabled_result = None
                if requested:
                    try:
                        enabled_result = enable_callback(agent, requested)
                    except Exception as err:  # pragma: no cover - defensive enabling
                        logger.error("search_tools.%s: enable_tools failed: %s", provider_name, err)
                else:
                    # Inner LLM didn't call enable_tools - log for debugging
                    logger.info(
                        "search_tools.%s: inner LLM did not call enable_tools for query '%s'; "
                        "LLM response: %s",
                        provider_name,
                        query[:80] if query else "",
                        (content_text[:200] + "...") if content_text and len(content_text) > 200 else content_text,
                    )

                named_fallback_selected = False
                if not requested and not requested_agent_skills and not requested_skills and not requested_system_skills:
                    fallback_skills = _fallback_named_selection(query or "", available_global_skill_names)
                    if fallback_skills:
                        named_fallback_selected = True
                        try:
                            enabled_global_skills_result = enable_global_skills(
                                agent,
                                fallback_skills,
                                available_skills=global_skills,
                            )
                            logger.info(
                                "search_tools.%s: exact-name fallback enabled global skills: %s",
                                provider_name,
                                ", ".join(fallback_skills),
                            )
                        except Exception as err:  # pragma: no cover - defensive enabling
                            logger.error("search_tools.%s: fallback enable_global_skills failed: %s", provider_name, err)

                    fallback_agent_skills = _fallback_named_selection(query or "", available_agent_skill_names)
                    if fallback_agent_skills:
                        named_fallback_selected = True
                        try:
                            enabled_agent_skills_result = _enable_agent_skills(
                                agent,
                                fallback_agent_skills,
                                available_skills=agent_skills,
                            )
                            logger.info(
                                "search_tools.%s: exact-name fallback enabled agent skills: %s",
                                provider_name,
                                ", ".join(fallback_agent_skills),
                            )
                        except Exception as err:  # pragma: no cover - defensive enabling
                            logger.error("search_tools.%s: fallback enable_agent_skills failed: %s", provider_name, err)

                    fallback_system_skills = _fallback_named_selection(query or "", available_system_skill_keys)
                    if fallback_system_skills:
                        named_fallback_selected = True
                        try:
                            enabled_system_skills_result = enable_system_skills(
                                agent,
                                fallback_system_skills,
                                available_skills=system_skills,
                            )
                            logger.info(
                                "search_tools.%s: exact-name fallback enabled system skills: %s",
                                provider_name,
                                ", ".join(fallback_system_skills),
                            )
                        except Exception as err:  # pragma: no cover - defensive enabling
                            logger.error("search_tools.%s: fallback enable_system_skills failed: %s", provider_name, err)

                message_lines: List[str] = []
                if content_text:
                    message_lines.append(content_text.strip())
                _append_section_summary(
                    message_lines,
                    enabled_global_skills_result,
                    enabled_label="Enabled skills",
                    already_enabled_label="Already enabled skills",
                    conflicts_label="Skill conflicts",
                    failed_label="Failed skills",
                )
                _append_section_summary(
                    message_lines,
                    enabled_agent_skills_result,
                    enabled_label="Refreshed agent skills",
                    already_enabled_label="Already refreshed agent skills",
                    evicted_label="Evicted (LRU)",
                    invalid_label="Invalid agent skills",
                )
                _append_section_summary(
                    message_lines,
                    enabled_system_skills_result,
                    enabled_label="Enabled system skills",
                    already_enabled_label="Already enabled system skills",
                    evicted_label="Evicted (LRU)",
                    invalid_label="Invalid system skills",
                )
                if enabled_system_skills_result and enabled_system_skills_result.get("pipedream_apps"):
                    _append_section_summary(
                        message_lines,
                        enabled_system_skills_result.get("pipedream_apps"),
                        enabled_label="Enabled apps",
                        already_enabled_label="Already enabled apps",
                        invalid_label="Invalid apps",
                    )
                _append_section_summary(
                    message_lines,
                    enabled_result,
                    enabled_label="Enabled tools",
                    already_enabled_label="Already enabled tools",
                    evicted_label="Evicted (LRU)",
                    invalid_label="Invalid tools",
                )

                # Fallback only when the model made no explicit tool or skill selection.
                if (
                    not requested
                    and not requested_agent_skills
                    and not requested_skills
                    and not requested_system_skills
                    and not named_fallback_selected
                ):
                    fallback = _fallback_builtin_selection(query or "", content_text or "", available_names)
                    fallback = [name for name in fallback if name in available_names]
                    if fallback:
                        try:
                            enabled_result = enable_callback(agent, fallback)
                            logger.info(
                                "search_tools.%s: heuristically enabled tools (no tool call): %s",
                                provider_name,
                                ", ".join(fallback),
                            )
                            _append_section_summary(
                                message_lines,
                                enabled_result,
                                enabled_label="Enabled tools",
                                already_enabled_label="Already enabled tools",
                                evicted_label="Evicted (LRU)",
                                invalid_label="Invalid tools",
                            )
                        except Exception as err:  # pragma: no cover - defensive enabling
                            logger.error("search_tools.%s: fallback enable_tools failed: %s", provider_name, err)

                # Build explicit message about what happened
                skills_were_enabled = (
                    enabled_global_skills_result
                    and enabled_global_skills_result.get("status") == "success"
                    and (
                        enabled_global_skills_result.get("enabled")
                        or enabled_global_skills_result.get("already_enabled")
                    )
                )
                agent_skills_were_enabled = (
                    enabled_agent_skills_result
                    and enabled_agent_skills_result.get("status") == "success"
                    and (
                        enabled_agent_skills_result.get("enabled")
                        or enabled_agent_skills_result.get("already_enabled")
                    )
                )
                system_skills_were_enabled = (
                    enabled_system_skills_result
                    and enabled_system_skills_result.get("status") == "success"
                    and (
                        enabled_system_skills_result.get("enabled")
                        or enabled_system_skills_result.get("already_enabled")
                    )
                )
                tools_were_enabled = enabled_result and enabled_result.get("status") == "success" and (
                    enabled_result.get("enabled") or enabled_result.get("already_enabled")
                )

                if (
                    not message_lines
                    and not tools_were_enabled
                    and not agent_skills_were_enabled
                    and not skills_were_enabled
                    and not system_skills_were_enabled
                ):
                    # Make it explicit when no tools were enabled
                    message_lines.append(
                        "No matching tools or skills found for your query. "
                        "Try a more specific query like 'linkedin profile' or 'crunchbase company', "
                        "or use the web search/scrape tools from the available list."
                    )

                response_payload: ToolSearchResult = {
                    "status": "success",
                    "message": "\n".join([line for line in message_lines if line]) or "",
                }
                if enabled_global_skills_result and enabled_global_skills_result.get("status") == "success":
                    response_payload["skills"] = _section_payload(
                        enabled=enabled_global_skills_result.get("enabled", []),
                        already_enabled=enabled_global_skills_result.get("already_enabled", []),
                        conflicts=enabled_global_skills_result.get("conflicts", []),
                    )
                    if enabled_global_skills_result.get("failed"):
                        response_payload["skills"]["failed"] = list(
                            enabled_global_skills_result.get("failed", [])
                        )
                if enabled_agent_skills_result and enabled_agent_skills_result.get("status") == "success":
                    response_payload["agent_skills"] = _section_payload(
                        enabled=enabled_agent_skills_result.get("enabled", []),
                        already_enabled=enabled_agent_skills_result.get("already_enabled", []),
                        evicted=enabled_agent_skills_result.get("evicted", []),
                        invalid=enabled_agent_skills_result.get("invalid", []),
                    )
                    if enabled_agent_skills_result.get("tools"):
                        response_payload["agent_skills"]["tools"] = enabled_agent_skills_result["tools"]
                if enabled_system_skills_result and enabled_system_skills_result.get("status") == "success":
                    response_payload["system_skills"] = _section_payload(
                        enabled=enabled_system_skills_result.get("enabled", []),
                        already_enabled=enabled_system_skills_result.get("already_enabled", []),
                        evicted=enabled_system_skills_result.get("evicted", []),
                        invalid=enabled_system_skills_result.get("invalid", []),
                    )
                    if enabled_system_skills_result.get("pipedream_apps"):
                        response_payload["system_skills"]["pipedream_apps"] = enabled_system_skills_result[
                            "pipedream_apps"
                        ]
                if enabled_result and enabled_result.get("status") == "success":
                    response_payload["tools"] = _section_payload(
                        enabled=enabled_result.get("enabled", []),
                        already_enabled=enabled_result.get("already_enabled", []),
                        evicted=enabled_result.get("evicted", []),
                        invalid=enabled_result.get("invalid", []),
                    )
                # Include external resources if any were suggested
                if external_resources:
                    response_payload["external_resources"] = external_resources
                return response_payload

            except Exception as exc:  # pragma: no cover - failover loop
                last_exc = exc
                logger.warning(
                    "search_tools.%s: provider %s failed for tool search: %s",
                    provider_name,
                    provider,
                    exc,
                )
                continue

        logger.error("search_tools.%s: all providers failed for tool search: %s", provider_name, last_exc)
        return {"status": "error", "message": "Failed to search tools"}

    except LLMNotConfiguredError:
        logger.warning("search_tools.%s: skipped because LLM configuration is missing", provider_name)
        return {
            "status": "error",
            "message": "Tool search is unavailable until the initial LLM setup is complete.",
            "reason": "llm_not_configured",
        }
    except Exception as exc:  # pragma: no cover - top-level guard
        logger.error("search_tools.%s: unexpected error during search: %s", provider_name, exc)
        return {"status": "error", "message": str(exc)}

def search_tools(agent: PersistentAgent, query: str) -> ToolSearchResult:
    """Search across MCP and builtin tools in a single LLM call."""
    manager = get_mcp_manager()
    if not manager._initialized:
        manager.initialize()

    blacklisted_tools = get_agent_tool_blacklist(agent)
    mcp_tools = [
        tool for tool in manager.get_tools_for_agent(agent)
        if tool.full_name not in blacklisted_tools
    ]

    builtin_catalog: List[Dict[str, Any]] = [
        {
            "full_name": entry.full_name,
            "description": entry.description,
            "parameters": entry.parameters,
        }
        for entry in get_available_builtin_tool_entries(agent).values()
    ]
    hidden_builtin_names = set(
        get_available_builtin_tool_entries(agent, include_hidden=True).keys()
    )
    custom_catalog: List[Dict[str, Any]] = [
        {
            "full_name": entry.full_name,
            "description": entry.description,
            "parameters": entry.parameters,
        }
        for entry in get_available_custom_tool_entries(agent).values()
    ]
    eval_synthetic_catalog = [
        entry
        for entry in get_available_eval_synthetic_tool_catalog(agent)
        if entry["full_name"] not in blacklisted_tools
    ]
    global_skill_catalog = get_compatible_global_skills(agent)
    agent_skill_catalog = _filter_agent_skills_for_tool_blacklist(
        get_latest_skill_versions(agent),
        blacklisted_tools,
    )
    system_skill_catalog = shortlist_system_skills(
        query,
        available_tool_names=hidden_builtin_names,
    )

    combined_catalog: List[Any] = list(mcp_tools) + builtin_catalog + custom_catalog + eval_synthetic_catalog
    pipedream_app_catalog: list[Any] = []
    enabled_app_slugs: list[str] = []
    owner = getattr(agent, "organization", None) or getattr(agent, "user", None)
    auto_enable_apps = True
    if owner is not None:
        auto_enable_apps = get_tool_settings_for_owner(owner).tool_search_auto_enable_apps
    if _has_active_pipedream_runtime():
        try:
            pipedream_app_catalog = PipedreamCatalogService().search_apps(query, limit=20)
            enabled_app_slugs = get_effective_pipedream_app_slugs_for_agent(agent)
        except PipedreamCatalogError as exc:
            logger.warning("search_tools: unable to search Pipedream apps for agent %s: %s", agent.id, exc)

    if not combined_catalog and not agent_skill_catalog and not global_skill_catalog and not system_skill_catalog and not pipedream_app_catalog:
        logger.info("search_tools: no tools available for agent %s", agent.id)
        return {"status": "success", "tools": [], "message": "No tools available"}

    return _search_with_llm(
        agent=agent,
        query=query,
        provider_name="catalog",
        catalog=combined_catalog,
        enable_callback=enable_tools,
        empty_message="No tools available",
        agent_skill_catalog=agent_skill_catalog,
        global_skill_catalog=global_skill_catalog,
        system_skill_catalog=system_skill_catalog,
        enable_apps_callback=(
            partial(
                enable_pipedream_apps_for_agent,
                available_app_slugs=[_tool_attr(app, "slug") for app in pipedream_app_catalog],
            )
            if pipedream_app_catalog and auto_enable_apps
            else None
        ),
        pipedream_app_catalog=pipedream_app_catalog,
        enabled_app_slugs=enabled_app_slugs,
        auto_enable_apps=auto_enable_apps,
    )


def get_search_tools_tool() -> Dict[str, Any]:
    """Return the search_tools tool definition for the LLM."""
    return {
        "type": "function",
        "function": {
            "name": "search_tools",
            "description": (
                "Discover and enable the right tools and skills for the current task, including saved custom tools. "
                "Use this when no currently enabled tool clearly fits, or when you need to choose between structured extractors, web search, scraping, browser automation, or a specialized integration. "
                "Use already-enabled direct tools such as sqlite_batch, create_csv, or a named integration tool; do not rediscover a matching enabled app/tool. "
                "Call it again when the task changes and you need different capabilities."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "Description of what you want to accomplish or what kind of tools and skills you're looking for",
                    }
                },
                "required": ["query"],
            },
        },
    }


@tracer.start_as_current_span("AGENT TOOL Search Tools")
def execute_search_tools(agent: PersistentAgent, params: Dict[str, Any]) -> ToolSearchResult:
    """Execute the search_tools function to find relevant tools."""
    span = trace.get_current_span()
    span.set_attribute("persistent_agent.id", str(agent.id))

    query = params.get("query")
    if not query:
        return {"status": "error", "message": "Missing required parameter: query"}

    span.set_attribute("search.query", query)
    logger.info("Agent %s searching for tools: %s", agent.id, query)

    return search_tools(agent, query)
