"""Built-in tool that returns the list of tools available to a profile."""

from __future__ import annotations

from navi.mcp.tools import build_mcp_name

from ._internal.base import Tool, ToolContext, ToolResult, current_profile_id

NATIVE_SOURCE = "native"
SCOPE_AGENT = "agent"
SCOPE_SUBAGENT = "subagent"
_MISSING_HINT = (
    "Declared for this profile but not registered — their MCP server is not "
    "connected (or the tool is gone). Do not call these; pick another tool."
)


def _manual_names() -> set[str]:
    """Tools with a hand-written manual. Imported lazily — tool_manual owns the
    manuals directory, and neither module should pull the other in at import time.
    """
    from navi.tools.tool_manual import manual_names

    return manual_names()


class ListToolsTool(Tool):
    name = "list_tools"
    description = (
        "Returns the tools a profile can actually use, grouped by source: its built-in "
        "tools as 'native' and each MCP server as 'mcp__<server>__'. Defaults to the "
        "profile you are running as — name another with profile_id. scope picks that "
        "profile's agent or subagent toolset (spawn_agent runs the latter). Names only "
        "by default — narrow it with query (substring over names and descriptions) or "
        "add verbose for descriptions. For one tool's full docs use tool_manual."
    )
    parameters = {
        "type": "object",
        "properties": {
            "profile_id": {
                "type": "string",
                "description": (
                    "Profile ID to inspect. Omit to inspect the profile you are currently "
                    "running as."
                ),
            },
            "scope": {
                "type": "string",
                "enum": [SCOPE_AGENT, SCOPE_SUBAGENT],
                "description": (
                    "Which of that profile's two toolsets to list: 'agent' (what this "
                    "profile calls directly, the default) or 'subagent' (what a "
                    "spawn_agent sub-agent running this profile gets)."
                ),
            },
            "query": {
                "type": "string",
                "description": (
                    "Optional substring filter over tool names and descriptions, "
                    "e.g. 'ssh' or 'notify'. The full catalogue of an MCP-heavy profile "
                    "is tens of thousands of characters — filter instead of pulling it all."
                ),
            },
            "verbose": {
                "type": "boolean",
                "description": (
                    "Add each tool's description. The output grows ~10x, so use it with a "
                    "narrow query, not to list everything."
                ),
            },
        },
    }

    def __init__(
        self,
        registry=None,
        profile_registry=None,
        mcp_manager=None,
    ) -> None:
        self._registry = registry
        self._profile_registry = profile_registry
        self._mcp_manager = mcp_manager

    async def execute(self, params: dict, ctx: ToolContext | None = None) -> ToolResult:
        if self._registry is None:
            return ToolResult(success=False, output="Registry not available.", error="no_registry")

        profile_id = (params.get("profile_id") or "").strip()
        # No profile named: report on the one this run executes as, so the common
        # question ("what do I have?") needs no argument the agent may not know.
        defaulted = not profile_id
        if defaulted:
            profile_id = (ctx.profile_id if ctx else None) or current_profile_id.get() or ""
        if not profile_id:
            return ToolResult(
                success=False,
                output=(
                    "No profile named and none is active in this context. Pass profile_id "
                    "explicitly — list_profiles shows the available IDs."
                ),
                error="no_active_profile",
            )

        if self._profile_registry is None:
            return ToolResult(
                success=False, output="Profile registry not available.", error="no_profile_registry"
            )

        try:
            profile = self._profile_registry.get(profile_id)
        except Exception:
            return ToolResult(
                success=False, output=f"Profile '{profile_id}' not found.", error="profile_not_found"
            )

        scope = (params.get("scope") or SCOPE_AGENT).strip().lower()
        if scope not in (SCOPE_AGENT, SCOPE_SUBAGENT):
            return ToolResult(
                success=False,
                output=f"Unknown scope '{scope}'. Use '{SCOPE_AGENT}' or '{SCOPE_SUBAGENT}'.",
                error="unknown_scope",
            )

        query = (params.get("query") or "").strip().lower()
        verbose = bool(params.get("verbose"))

        declared = self._declared_by_source(profile, scope)
        declared_total = sum(len(names) for names in declared.values())

        # Resolve against the LIVE registry, not the config: a tool the config
        # declares but nothing registered (server down, tool removed) must not be
        # advertised — the agent would call it and get "tool not found".
        available: dict[str, list[str]] = {}
        descriptions: dict[str, str] = {}
        missing: list[str] = []
        matched = 0
        for source, names in declared.items():
            kept: list[str] = []
            for name in names:
                tool = self._lookup(name)
                if tool is None:
                    if not query or query in name.lower():
                        missing.append(name)
                    continue
                description = tool.description or ""
                if query and query not in name.lower() and query not in description.lower():
                    continue
                kept.append(name)
                descriptions[name] = description
                matched += 1
            if kept:
                available[source] = kept

        lines = [self._header(profile_id, matched, declared_total, query, scope, defaulted)]
        for source in self._ordered_sources(available):
            names = available[source]
            if verbose:
                lines.append(f"\n{source} ({len(names)})")
                lines.extend(f"  • {n}: {descriptions.get(n, '')}" for n in names)
            else:
                lines.append(f"{source} ({len(names)}): {', '.join(names)}")

        if missing:
            lines.append("")
            lines.append(f"Not registered ({len(missing)}): {', '.join(missing)}")
            lines.append(_MISSING_HINT)

        # One line, not one per tool: which of this profile's tools have a real
        # hand-written manual is the difference between "read tool_manual" being a
        # useful suggestion and a name the agent has no reason to pick over another.
        documented = sorted(
            name for names in available.values() for name in names if name in _manual_names()
        )
        if documented:
            lines.append("")
            lines.append(f"Hand-written manuals available: {', '.join(documented)}")

        if not query and not verbose:
            lines.append("")
            lines.append(
                "Pass query=… to filter, verbose=true for descriptions, tool_manual for one tool's docs."
            )

        return ToolResult(success=True, output="\n".join(lines))

    # ── helpers ──────────────────────────────────────────────────────────

    def _declared_by_source(self, profile, scope: str = SCOPE_AGENT) -> dict[str, list[str]]:
        """Names the profile enables, bucketed by source ('native', 'mcp__<server>__')."""
        scope_config = (
            profile.get_subagent_tools() if scope == SCOPE_SUBAGENT else profile.get_agent_tools()
        )
        groups: dict[str, list[str]] = {NATIVE_SOURCE: list(scope_config.native)}

        # Same reader the agent itself uses — a second, cwd-relative copy of
        # this path made list_tools disagree with the real toolset.
        from navi.core.tool_utils import load_user_enabled_tools

        for name in load_user_enabled_tools():
            if name not in groups[NATIVE_SOURCE]:
                groups[NATIVE_SOURCE].append(name)

        if scope_config.mcp and self._mcp_manager:
            for server_name, group_names in scope_config.mcp.items():
                source = build_mcp_name(server_name, "")
                bucket = groups.setdefault(source, [])
                if "*" in group_names:
                    for tool in self._registry.all():
                        if tool.name.startswith(source) and tool.name not in bucket:
                            bucket.append(tool.name)
                else:
                    for group_name in group_names:
                        for tool_name in self._mcp_manager.resolve_group(server_name, group_name):
                            full_name = build_mcp_name(server_name, tool_name)
                            if full_name not in bucket:
                                bucket.append(full_name)

        return {source: names for source, names in groups.items() if names}

    def _lookup(self, name: str):
        try:
            return self._registry.get(name)
        except Exception:
            return None

    @staticmethod
    def _ordered_sources(available: dict[str, list[str]]) -> list[str]:
        """Native tools first, then MCP servers alphabetically."""
        servers = sorted(source for source in available if source != NATIVE_SOURCE)
        return ([NATIVE_SOURCE] if NATIVE_SOURCE in available else []) + servers

    @staticmethod
    def _header(
        profile_id: str,
        matched: int,
        declared_total: int,
        query: str,
        scope: str,
        defaulted: bool,
    ) -> str:
        # Say which profile was assumed and which toolset was read — an agent that
        # asked about nobody must not read a subagent list as its own tools.
        notes = []
        if defaulted:
            notes.append("current profile")
        if scope == SCOPE_SUBAGENT:
            notes.append(f"{SCOPE_SUBAGENT} scope")
        prefix = f"Tools for profile '{profile_id}'"
        if notes:
            prefix += f" ({', '.join(notes)})"
        if query:
            return f"{prefix} matching '{query}': {matched} of {declared_total} declared."
        return f"{prefix} ({matched} available):"
