"""Consolidated ConfigManager tool for inspecting and controlling the MindRoom configuration."""

from __future__ import annotations

import re
from copy import deepcopy
from dataclasses import replace
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal

import yaml
from agno.tools import Toolkit
from pydantic import BaseModel, ConfigDict, JsonValue, ValidationError, model_validator

from mindroom.api.config_lifecycle import persist_runtime_validated_config, validate_and_persist_config_payload
from mindroom.authorization import (
    is_sender_allowed_for_agent_credential_management,
    responder_candidate_entities_from_cached_room,
)
from mindroom.commands.parsing import get_command_help
from mindroom.config.agent import AgentConfig, TeamConfig
from mindroom.config.main import (
    Config,
    ConfigRuntimeValidationError,
    format_invalid_config_message,
    load_config_or_user_error,
)
from mindroom.config.models import AgentLearningMode, ToolConfigEntry
from mindroom.entity_resolution import entity_identity_registry
from mindroom.logging_config import get_logger
from mindroom.oauth import oauth_connect_url_requires_host_browser
from mindroom.oauth.registry import load_oauth_providers
from mindroom.oauth.service import oauth_connect_url
from mindroom.redaction import redact_sensitive_data
from mindroom.tool_system.catalog import ToolCategory, ToolStatus, resolved_tool_metadata_for_runtime
from mindroom.tool_system.runtime_context import (
    build_execution_identity_from_runtime_context,
    get_tool_runtime_context,
)
from mindroom.tool_system.worker_routing import build_agent_toolkit_worker_target
from mindroom.yaml_io import safe_dump

if TYPE_CHECKING:
    from collections.abc import Sequence

    from mindroom.constants import RuntimePaths
    from mindroom.tool_system.catalog import ToolMetadata

logger = get_logger(__name__)
_CONFIG_CHANGE_REJECTED_MESSAGE = "Changes were NOT applied."
_AgentScope = Literal["current_room", "all"]
_VALID_AGENT_SCOPES = {"current_room", "all"}
_MAX_CONFIG_INSPECTION_CHARS = 20_000


class _ConfigPatchError(ValueError):
    """One invalid JSON Pointer or patch operation."""


class _ConfigPatchChange(BaseModel):
    """One schema-visible authored configuration patch entry."""

    model_config = ConfigDict(extra="forbid")

    op: Literal["add", "replace", "remove"]
    path: str
    value: JsonValue = None

    @model_validator(mode="after")
    def _require_value_exactly_for_writes(self) -> _ConfigPatchChange:
        has_value = "value" in self.model_fields_set
        if self.op == "remove":
            if has_value:
                msg = "op 'remove' does not take a value"
                raise ValueError(msg)
        elif not has_value:
            msg = f"op {self.op!r} requires a value"
            raise ValueError(msg)
        return self


def _normalize_patch_changes(
    changes: Sequence[_ConfigPatchChange | dict[str, Any]],
) -> list[_ConfigPatchChange]:
    """Normalize tool-decoded models and direct-call dictionaries."""
    normalized: list[_ConfigPatchChange] = []
    for position, change in enumerate(changes):
        try:
            model = change if isinstance(change, _ConfigPatchChange) else _ConfigPatchChange.model_validate(change)
        except ValidationError as exc:
            details = "; ".join(
                f"changes[{position}]{'.' if error['loc'] else ''}{'.'.join(map(str, error['loc']))}: {error['msg']}"
                for error in exc.errors(include_input=False, include_url=False)
            )
            msg = f"Invalid patch request: {details}"
            raise _ConfigPatchError(msg) from exc
        normalized.append(model)
    return normalized


def _reject_normalized_root_nulls(changes: list[_ConfigPatchChange], validated_authored: dict[str, Any]) -> None:
    """Reject root nulls that the Config schema silently normalizes away."""
    missing = object()
    for change in changes:
        if change.op == "remove" or change.value is not None:
            continue
        tokens = _decode_json_pointer(change.path)
        if len(tokens) != 1 or validated_authored.get(tokens[0], missing) is None:
            continue
        msg = (
            f"{change.path!r} cannot be set to null because the schema normalizes null "
            "to unset/default; use remove to unset it or provide a non-null value"
        )
        raise _ConfigPatchError(msg)


def _decode_json_pointer(path: str) -> list[str]:
    """Decode one RFC 6901 JSON Pointer into reference tokens."""
    if path == "":
        return []
    if not path.startswith("/"):
        msg = f"JSON Pointer must be empty for the root or start with '/': {path!r}"
        raise _ConfigPatchError(msg)

    decoded: list[str] = []
    for raw_token in path[1:].split("/"):
        token: list[str] = []
        index = 0
        while index < len(raw_token):
            character = raw_token[index]
            if character != "~":
                token.append(character)
                index += 1
                continue
            if index + 1 >= len(raw_token) or raw_token[index + 1] not in {"0", "1"}:
                msg = f"Invalid JSON Pointer escape in {path!r}; only '~0' and '~1' are allowed"
                raise _ConfigPatchError(msg)
            token.append("~" if raw_token[index + 1] == "0" else "/")
            index += 2
        decoded.append("".join(token))
    return decoded


def _array_index(reference_token: str, *, length: int, allow_append: bool, path: str) -> int:
    """Return one validated RFC 6902 array index."""
    if reference_token == "-":  # noqa: S105 - JSON Patch append sentinel, not a password
        if allow_append:
            return length
        msg = f"Array append token '-' is only valid for add operations: {path!r}"
        raise _ConfigPatchError(msg)
    if (
        not reference_token.isascii()
        or not reference_token.isdigit()
        or (len(reference_token) > 1 and reference_token.startswith("0"))
    ):
        msg = f"Invalid array index {reference_token!r} in JSON Pointer {path!r}"
        raise _ConfigPatchError(msg)
    return int(reference_token)


def _resolve_json_pointer(document: Any, path: str) -> Any:  # noqa: ANN401
    """Resolve one JSON Pointer against an authored config document."""
    current = document
    for token in _decode_json_pointer(path):
        if isinstance(current, dict):
            if token not in current:
                msg = f"Path does not exist in the authored configuration: {path!r}"
                raise _ConfigPatchError(msg)
            current = current[token]
            continue
        if isinstance(current, list):
            index = _array_index(token, length=len(current), allow_append=False, path=path)
            if index >= len(current):
                msg = f"Array index {index} is out of range in JSON Pointer {path!r}"
                raise _ConfigPatchError(msg)
            current = current[index]
            continue
        msg = f"Cannot traverse through {type(current).__name__} in JSON Pointer {path!r}"
        raise _ConfigPatchError(msg)
    return current


def _resolve_patch_parent(document: Any, path: str) -> tuple[Any, str]:  # noqa: ANN401
    """Resolve the parent container and final token for one non-root patch path."""
    tokens = _decode_json_pointer(path)
    parent = document
    for token in tokens[:-1]:
        if isinstance(parent, dict):
            if token not in parent:
                msg = f"Parent path does not exist in the authored configuration: {path!r}"
                raise _ConfigPatchError(msg)
            parent = parent[token]
            continue
        if isinstance(parent, list):
            index = _array_index(token, length=len(parent), allow_append=False, path=path)
            if index >= len(parent):
                msg = f"Array index {index} is out of range in JSON Pointer {path!r}"
                raise _ConfigPatchError(msg)
            parent = parent[index]
            continue
        msg = f"Cannot traverse through {type(parent).__name__} in JSON Pointer {path!r}"
        raise _ConfigPatchError(msg)
    return parent, tokens[-1]


def _apply_mapping_patch(parent: dict[str, Any], change: _ConfigPatchChange, reference_token: str) -> None:
    """Apply one validated patch entry to a mapping parent."""
    if change.op == "replace" and reference_token not in parent:
        msg = f"Replace target does not exist in the authored configuration: {change.path!r}; use add instead"
        raise _ConfigPatchError(msg)
    if change.op == "remove":
        if reference_token not in parent:
            msg = f"Remove target does not exist in the authored configuration: {change.path!r}"
            raise _ConfigPatchError(msg)
        del parent[reference_token]
        return
    parent[reference_token] = deepcopy(change.value)


def _apply_list_patch(parent: list[Any], change: _ConfigPatchChange, reference_token: str) -> None:
    """Apply one validated patch entry to an array parent."""
    index = _array_index(reference_token, length=len(parent), allow_append=change.op == "add", path=change.path)
    if change.op == "add":
        if index > len(parent):
            msg = f"Array index {index} is out of range for add in JSON Pointer {change.path!r}"
            raise _ConfigPatchError(msg)
        parent.insert(index, deepcopy(change.value))
        return
    if index >= len(parent):
        verb = "Replace" if change.op == "replace" else "Remove"
        suffix = "; use add instead" if change.op == "replace" else ""
        msg = f"{verb} target does not exist in the authored configuration: {change.path!r}{suffix}"
        raise _ConfigPatchError(msg)
    if change.op == "replace":
        parent[index] = deepcopy(change.value)
    else:
        del parent[index]


def _apply_one_config_patch(candidate: Any, change: _ConfigPatchChange) -> Any:  # noqa: ANN401
    """Apply one validated patch entry and return the possibly replaced root."""
    if change.path == "":
        if change.op == "remove":
            msg = "The document root cannot be removed; replace it with an object instead"
            raise _ConfigPatchError(msg)
        return deepcopy(change.value)

    parent, reference_token = _resolve_patch_parent(candidate, change.path)
    if isinstance(parent, dict):
        _apply_mapping_patch(parent, change, reference_token)
    elif isinstance(parent, list):
        _apply_list_patch(parent, change, reference_token)
    else:
        msg = f"Patch parent is not a container in JSON Pointer {change.path!r}"
        raise _ConfigPatchError(msg)
    return candidate


def _apply_config_patch(
    document: dict[str, Any],
    changes: list[_ConfigPatchChange],
) -> tuple[dict[str, Any], list[str]]:
    """Apply an atomic add/replace/remove patch to a copied config document."""
    if not changes:
        msg = "Patch operation requires a non-empty changes list"
        raise _ConfigPatchError(msg)

    candidate: Any = deepcopy(document)
    for change in changes:
        candidate = _apply_one_config_patch(candidate, change)

    if not isinstance(candidate, dict):
        msg = "The authored configuration root must be an object"
        raise _ConfigPatchError(msg)
    return candidate, [change.path for change in changes]


def _is_known_tool_entry(tool_name: str, tool_metadata: dict[str, ToolMetadata]) -> bool:
    """Return whether a tool entry is a known registered tool."""
    return tool_name in tool_metadata


def preserve_tool_overrides(
    existing_entries: list[ToolConfigEntry],
    updated_tool_names: list[str],
) -> list[ToolConfigEntry]:
    """Keep inline overrides for retained tools during string-only tool-list edits."""
    existing_by_name = {entry.name: entry for entry in existing_entries}
    return [existing_by_name.get(tool_name, ToolConfigEntry(name=tool_name)) for tool_name in updated_tool_names]


def validate_knowledge_bases(
    knowledge_bases: list[str],
    configured_knowledge_bases: set[str],
) -> str | None:
    """Validate that all requested knowledge base IDs exist in config.

    Returns an error message string if validation fails, None otherwise.
    """
    seen: set[str] = set()
    duplicates: list[str] = []
    for base_id in knowledge_bases:
        if base_id in seen and base_id not in duplicates:
            duplicates.append(base_id)
        seen.add(base_id)
    if duplicates:
        return f"Error: Duplicate knowledge bases are not allowed: {', '.join(duplicates)}."

    invalid_knowledge_bases = sorted(
        {base_id for base_id in knowledge_bases if base_id not in configured_knowledge_bases},
    )
    if not invalid_knowledge_bases:
        return None

    invalid = ", ".join(invalid_knowledge_bases)
    available = ", ".join(sorted(configured_knowledge_bases))
    if not available:
        return f"Error: Unknown knowledge bases: {invalid}. No knowledge bases are configured."
    return f"Error: Unknown knowledge bases: {invalid}. Available knowledge bases: {available}."


def _build_oauth_onboarding_guidance(
    config: Config,
    runtime_paths: RuntimePaths,
    agent_name: str,
    previous_tools: list[str],
    updated_tools: list[str],
    tool_metadata: dict[str, ToolMetadata],
) -> str:
    """Return target-scoped connect links for newly enabled generic OAuth providers."""
    previous_provider_ids = {
        metadata.auth_provider
        for tool_name in previous_tools
        if (metadata := tool_metadata.get(tool_name)) is not None and metadata.auth_provider is not None
    }
    added_tools_by_provider: dict[str, list[str]] = {}
    for tool_name in updated_tools:
        metadata = tool_metadata.get(tool_name)
        if metadata is None or metadata.auth_provider is None or metadata.auth_provider in previous_provider_ids:
            continue
        added_tools_by_provider.setdefault(metadata.auth_provider, []).append(tool_name)
    if not added_tools_by_provider:
        return ""

    runtime_context = get_tool_runtime_context()
    if runtime_context is None:
        return ""
    if not is_sender_allowed_for_agent_credential_management(
        runtime_context.requester_id,
        agent_name=agent_name,
        config=config,
    ):
        return (
            f"\n\nNo OAuth connect link was issued because the current requester is not authorized to manage "
            f"credentials for agent `{agent_name}`."
        )

    caller_identity = build_execution_identity_from_runtime_context(runtime_context)
    target_identity = replace(
        caller_identity,
        agent_name=agent_name,
        transport_agent_name=agent_name,
    )
    agent = config.get_agent(agent_name)
    worker_target = build_agent_toolkit_worker_target(
        config.resolve_entity(agent_name).execution_scope,
        agent_name,
        is_private=agent.private is not None,
        execution_identity=target_identity,
        runtime_paths=runtime_paths,
    )
    providers = load_oauth_providers(config, runtime_paths)
    link_lines: list[str] = []
    any_requires_host_browser = False
    for provider_id, tool_names in added_tools_by_provider.items():
        provider = providers.get(provider_id)
        if provider is None:
            continue
        connect_url = oauth_connect_url(provider, runtime_paths, worker_target=worker_target)
        requires_host_browser = oauth_connect_url_requires_host_browser(connect_url)
        any_requires_host_browser = any_requires_host_browser or requires_host_browser
        tools_label = ", ".join(f"`{tool_name}`" for tool_name in tool_names)
        link_lines.append(
            f"- {tools_label}: `connect_url`: {connect_url}; "
            f"`requires_host_browser`: {str(requires_host_browser).lower()}",
        )
    if not link_lines:
        return ""

    host_browser_guidance = ""
    if any_requires_host_browser:
        host_browser_guidance = (
            " Localhost links must be opened in a browser on the computer where MindRoom is running."
        )
    return (
        f"\n\n**Connect MindRoom-managed OAuth for agent `{agent_name}`:**\n"
        + "\n".join(link_lines)
        + "\nGive the user these direct links instead of sending them to the dashboard."
        + host_browser_guidance
        + f" After connection, have agent `{agent_name}` retry an appropriate safe status, read, or list operation. "
        "Newly configured tools are not guaranteed to be available to the current agent or in the current run."
    )


def _oauth_onboarding_guidance(
    config: Config,
    runtime_paths: RuntimePaths,
    agent_name: str,
    previous_tools: list[str],
    updated_tools: list[str],
    tool_metadata: dict[str, ToolMetadata],
) -> str:
    """Build optional OAuth guidance without changing a successful config write into an error."""
    try:
        return _build_oauth_onboarding_guidance(
            config,
            runtime_paths,
            agent_name,
            previous_tools,
            updated_tools,
            tool_metadata,
        )
    except Exception:
        logger.exception("oauth_onboarding_guidance_failed", agent_name=agent_name)
        return ""


class _InfoType(str, Enum):
    """Types of information that can be retrieved."""

    MINDROOM_DOCS = "mindroom_docs"
    CONFIG_SCHEMA = "config_schema"
    AVAILABLE_MODELS = "available_models"
    AGENTS = "agents"
    TEAMS = "teams"
    AVAILABLE_TOOLS = "available_tools"
    TOOL_DETAILS = "tool_details"
    AGENT_CONFIG = "agent_config"
    AGENT_TEMPLATE = "agent_template"


class ConfigManagerTools(Toolkit):
    """Consolidated tools for inspecting and controlling the MindRoom configuration.

    This toolkit provides full-configuration inspection and patching plus agent and
    team building with a minimal number of tools to reduce cognitive load on AI models.
    """

    def __init__(self, runtime_paths: RuntimePaths) -> None:
        """Initialize the ConfigManager toolkit.

        Args:
            runtime_paths: Explicit runtime context for config IO

        """
        self.runtime_paths = runtime_paths
        self.config_path = runtime_paths.config_path
        self._mindroom_docs: str | None = None
        self._help_text: str | None = None

        # Register only the consolidated tools
        super().__init__(
            name="config_manager",
            tools=[
                self.get_info,
                self.manage_config,
                self.manage_agent,
                self.manage_team,
            ],
        )

    def manage_config(
        self,
        operation: Literal["inspect", "patch"],
        path: str = "",
        changes: list[_ConfigPatchChange] | None = None,
        dry_run: bool = False,
    ) -> str:
        """Inspect or patch any authored MindRoom configuration field.

        This is full-configuration control. Both operations address the authored
        document written to ``config.yaml``: unset defaults and runtime overlays
        are not present. Paths use RFC 6901 JSON Pointer syntax; the empty string
        addresses the document root. Inspection output is always redacted, at any
        pointer depth; read raw values from the source files when truly required.
        Patch changes support RFC 6902 ``add``, ``replace``, and ``remove``. Use
        ``-`` as the final token to append to a list. All changes in one call are
        validated and persisted atomically.

        Args:
            operation: ``inspect`` to read one subtree or ``patch`` to change config.
            path: JSON Pointer to inspect. Only used with ``operation="inspect"``.
            changes: Atomic patch entries with ``op``, ``path``, and ``value``
                (except ``remove``, which has no value).
            dry_run: Validate a patch and return its receipt without writing.

        Returns:
            Redacted authored YAML for inspection, or a validation/persistence receipt.

        """
        if operation == "inspect":
            return self._inspect_authored_config(path=path, changes=changes, dry_run=dry_run)
        if operation == "patch":
            return self._patch_authored_config(path=path, changes=changes, dry_run=dry_run)
        return "Error: Unknown operation. Use 'inspect' or 'patch'."

    def _inspect_authored_config(  # noqa: PLR0911
        self,
        *,
        path: str,
        changes: list[_ConfigPatchChange] | None,
        dry_run: bool,
    ) -> str:
        """Return one redacted authored config subtree."""
        if changes is not None:
            return "Error: changes is only valid with operation='patch'."
        if dry_run:
            return "Error: dry_run is only valid with operation='patch'."

        config, load_error = self._load_config_or_error()
        if load_error:
            return load_error
        assert config is not None

        try:
            redacted_document = redact_sensitive_data(config.authored_model_dump())
            value = _resolve_json_pointer(redacted_document, path)
            rendered = safe_dump(
                value,
                default_flow_style=False,
                sort_keys=False,
                allow_unicode=True,
            ).rstrip()
            rendered = rendered.removesuffix("\n...").rstrip()
        except _ConfigPatchError as exc:
            return f"Error inspecting authored configuration: {exc}"
        except Exception as exc:
            logger.exception("config_inspection_failed", path=path)
            return f"Error inspecting authored configuration: {exc}"

        if len(rendered) > _MAX_CONFIG_INSPECTION_CHARS:
            return (
                f"Error: Authored configuration at {path or '<root>'!r} is too large to return "
                f"safely ({len(rendered)} characters). Inspect a narrower JSON Pointer path."
            )
        include_note = (
            "\n\n⚠️ This configuration is composed from multiple files via `!include`. "
            "Inspection shows the composed authored document, but structured patching is unavailable."
            if len(config.source_files) > 1
            else ""
        )
        return (
            "## Authored MindRoom configuration\n\n"
            f"- Config path: `{self.config_path}`\n"
            f"- JSON Pointer: `{path}`\n"
            "- View: authored values only; unset defaults and runtime overlays are excluded\n\n"
            f"```yaml\n{rendered}\n```"
            f"{include_note}"
        )

    def _patch_authored_config(  # noqa: PLR0911
        self,
        *,
        path: str,
        changes: list[_ConfigPatchChange] | None,
        dry_run: bool,
    ) -> str:
        """Validate and optionally persist one authored config patch."""
        if path:
            return "Error: path is only valid with operation='inspect'; use changes[].path for patches."
        if changes is None:
            return f"Error: operation='patch' requires changes.\n\n{_CONFIG_CHANGE_REJECTED_MESSAGE}"

        config, load_error = self._load_config_or_error(
            footer=_CONFIG_CHANGE_REJECTED_MESSAGE,
        )
        if load_error:
            return load_error
        assert config is not None
        if len(config.source_files) > 1:
            return (
                "Error: configuration is composed from multiple files via !include; "
                f"edit the source files instead.\n\n{_CONFIG_CHANGE_REJECTED_MESSAGE}"
            )

        try:
            authored = config.authored_model_dump()
            normalized_changes = _normalize_patch_changes(changes)
            candidate, changed_paths = _apply_config_patch(authored, normalized_changes)
            validated_config = Config.validate_with_runtime(candidate, self.runtime_paths)
            _reject_normalized_root_nulls(normalized_changes, validated_config.authored_model_dump())
            if not dry_run:
                persist_runtime_validated_config(validated_config, self.runtime_paths)
        except _ConfigPatchError as exc:
            return f"Error: {exc}\n\n{_CONFIG_CHANGE_REJECTED_MESSAGE}"
        except (ValidationError, ConfigRuntimeValidationError) as exc:
            return format_invalid_config_message(exc, footer=_CONFIG_CHANGE_REJECTED_MESSAGE)
        except Exception as exc:
            logger.exception("config_patch_failed")
            return f"Error patching configuration: {exc}\n\n{_CONFIG_CHANGE_REJECTED_MESSAGE}"

        return self._config_patch_receipt(changed_paths, dry_run=dry_run)

    def _config_patch_receipt(self, changed_paths: list[str], *, dry_run: bool) -> str:
        """Return a path-only receipt without leaking changed values."""
        status = "validated" if dry_run else "updated"
        paths = "\n".join(f"  - `{changed_path}`" for changed_path in changed_paths)
        runtime_note = (
            "No runtime state changed because this was a dry run."
            if dry_run
            else (
                "Matching registered API snapshots are published synchronously when present. "
                "The orchestrator applies the saved configuration through its normal reload "
                "after the current response; this receipt does not claim Matrix-side reconciliation."
            )
        )
        return (
            f"✅ Authored configuration patch {status}.\n\n"
            f"- Config path: `{self.config_path}`\n"
            "- Validated: yes\n"
            f"- Persisted: {'no (dry run)' if dry_run else 'yes'}\n"
            "- Changed paths:\n"
            f"{paths}\n\n"
            f"{runtime_note}"
        )

    def get_info(  # noqa: C901, PLR0911, PLR0912
        self,
        info_type: str,
        name: str | None = None,
        agent_scope: _AgentScope = "current_room",
    ) -> str:
        """Get various types of information about MindRoom, agents, tools, and configuration.

        Args:
            info_type: Type of information to retrieve. Options:
                - "mindroom_docs": MindRoom documentation and help
                - "config_schema": Configuration schema for agents and teams
                - "available_models": List of configured AI models
                - "agents": List agents in the current room by default
                - "teams": List all configured teams
                - "available_tools": List all available tools by category
                - "tool_details": Get details about a specific tool (requires name)
                - "agent_config": Get configuration for a specific agent (requires name)
                - "agent_template": Generate template for agent type (requires name as type)
            name: Optional name/identifier for specific queries (tool name, agent name, or template type)
            agent_scope: Agent listing scope. Use "current_room" to show current room agents or
                "all" for all configured agents.

        Returns:
            Requested information as formatted string

        """
        try:
            if info_type == _InfoType.MINDROOM_DOCS:
                return self._get_mindroom_info()
            if info_type == _InfoType.CONFIG_SCHEMA:
                return self._get_config_schema()
            if info_type == _InfoType.AVAILABLE_MODELS:
                return self._get_available_models()
            if info_type == _InfoType.AGENTS:
                return self._list_agents(agent_scope=agent_scope)
            if info_type == _InfoType.TEAMS:
                return self._list_teams()
            if info_type == _InfoType.AVAILABLE_TOOLS:
                return self._list_available_tools()
            if info_type == _InfoType.TOOL_DETAILS:
                if not name:
                    return "Error: tool_details requires 'name' parameter with the tool name"
                return self._get_tool_details(name)
            if info_type == _InfoType.AGENT_CONFIG:
                if not name:
                    return "Error: agent_config requires 'name' parameter with the agent name"
                return self._get_agent_config(name)
            if info_type == _InfoType.AGENT_TEMPLATE:
                if not name:
                    return "Error: agent_template requires 'name' parameter with the template type (researcher, developer, social, communicator, analyst, productivity)"
                return self._generate_agent_template(name)
            return f"Error: Unknown info_type '{info_type}'. Valid options: {', '.join([t.value for t in _InfoType])}"
        except Exception as e:
            logger.exception("config_info_lookup_failed", info_type=info_type)
            return f"Error getting {info_type}: {e}"

    def manage_agent(
        self,
        operation: Literal["create", "update", "validate"],
        agent_name: str,
        display_name: str | None = None,
        role: str | None = None,
        tools: list[str] | None = None,
        instructions: list[str] | None = None,
        model: str | None = None,
        rooms: list[str] | None = None,
        knowledge_bases: list[str] | None = None,
        include_default_tools: bool | None = None,
        markdown: bool | None = None,
        learning: bool | None = None,
        learning_mode: AgentLearningMode | None = None,
    ) -> str:
        """Manage agent configurations - create, update, or validate agents.

        Args:
            operation: Operation to perform - "create", "update", or "validate"
            agent_name: Internal name for the agent (alphanumeric, lowercase)
            display_name: Human-readable display name (required for create)
            role: Description of the agent's purpose (required for create)
            tools: List of tool names to enable for the agent
            instructions: List of instructions for the agent
            model: Model to use (default: "default")
            rooms: List of room IDs or names to auto-join
            knowledge_bases: List of knowledge base IDs to assign to this agent
            include_default_tools: Whether this agent should include defaults.tools
            markdown: Whether to use markdown formatting
            learning: Whether to enable Agno Learning for this agent
            learning_mode: Learning mode for Agno Learning ("always" or "agentic")

        Returns:
            Success message or error details

        """
        if operation == "create":
            if not display_name:
                return "Error: display_name is required for create operation"
            if role is None:
                role = ""
            return self._create_agent_config(
                agent_name=agent_name,
                display_name=display_name,
                role=role,
                tools=tools or [],
                instructions=instructions or [],
                model=model or "default",
                rooms=rooms or [],
                knowledge_bases=knowledge_bases or [],
                include_default_tools=include_default_tools,
                markdown=markdown,
                learning=learning,
                learning_mode=learning_mode,
            )
        if operation == "update":
            return self._update_agent_config(
                agent_name=agent_name,
                display_name=display_name,
                role=role,
                tools=tools,
                instructions=instructions,
                model=model,
                rooms=rooms,
                knowledge_bases=knowledge_bases,
                include_default_tools=include_default_tools,
                markdown=markdown,
                learning=learning,
                learning_mode=learning_mode,
            )
        if operation == "validate":
            return self._validate_agent_config(agent_name)
        return f"Error: Unknown operation '{operation}'. Valid options: create, update, validate"

    def manage_team(
        self,
        team_name: str,
        display_name: str,
        role: str,
        agents: list[str],
        mode: str = "coordinate",
    ) -> str:
        """Create or manage team configurations.

        Args:
            team_name: Internal name for the team
            display_name: Human-readable display name
            role: Description of the team's purpose
            agents: List of agent names that compose this team
            mode: Team mode - "coordinate" or "collaborate"

        Returns:
            Success message or error details

        """
        return self._create_team_config(team_name, display_name, role, agents, mode)

    # ===== Internal helper methods (not exposed as tools) =====

    def _load_mindroom_docs(self) -> str:
        """Load MindRoom documentation once and cache it."""
        if self._mindroom_docs is None:
            readme_path = Path(__file__).parent.parent.parent.parent / "README.md"
            try:
                with readme_path.open() as f:
                    self._mindroom_docs = f.read()
            except Exception as e:
                logger.warning("mindroom_readme_load_failed", error=str(e))
                self._mindroom_docs = "README.md not available"
        return self._mindroom_docs

    def _load_help_text(self) -> str:
        """Load help text once and cache it."""
        if self._help_text is None:
            self._help_text = get_command_help()
        return self._help_text

    def _load_config_or_error(
        self,
        *,
        footer: str | None = None,
    ) -> tuple[Config | None, str | None]:
        """Load config or return one shared invalid-config response."""
        return load_config_or_user_error(
            self.runtime_paths,
            footer=footer,
            tolerate_plugin_load_errors=True,
        )

    def _load_config_and_tool_metadata_or_error(
        self,
        *,
        footer: str | None = None,
    ) -> tuple[Config | None, dict[str, ToolMetadata] | None, str | None]:
        """Load config and resolve one runtime-aware tool metadata snapshot."""
        config, load_error = self._load_config_or_error(footer=footer)
        if load_error:
            return None, None, load_error
        assert config is not None
        return (
            config,
            resolved_tool_metadata_for_runtime(
                self.runtime_paths,
                config,
                tolerate_plugin_load_errors=True,
            ),
            None,
        )

    def _get_available_models(self) -> str:
        """Get the list of configured models from the current configuration."""
        config, load_error = self._load_config_or_error()
        if load_error:
            return load_error
        assert config is not None

        output = ["# Available Models\n"]

        if not config.models:
            return "No models configured in the system."

        output.append("These models are currently configured and can be used:\n")

        for model_name, model_config in config.models.items():
            provider = model_config.provider
            model_id = model_config.id

            output.append(f"## `{model_name}`")
            output.append(f"- **Provider**: {provider}")
            output.append(f"- **Model ID**: {model_id}")

            if model_config.host:
                output.append(f"- **Host**: {model_config.host}")

            if model_name == "default":
                output.append("- **Note**: This is typically the system default model")

            output.append("")

        if config.router and config.router.model:
            output.append("## Router Configuration")
            output.append(f"The router uses model: `{config.router.model}`")
            output.append("")

        return "\n".join(output)

    def _format_schema_field(self, field: str, info: dict, required_fields: list) -> list[str]:
        """Format a single schema field for display."""
        lines = []
        required = field in required_fields
        field_type = info.get("type", "unknown")
        description = info.get("description", "")
        default = info.get("default")

        if "enum" in info:
            field_type = f"enum: {info['enum']}"

        lines.append(f"{field}:  # {field_type}")
        if description:
            lines.append(f"  # {description}")
        if required:
            lines.append("  # REQUIRED")
        elif default is not None:
            lines.append(f"  # Default: {default}")
        lines.append("")
        return lines

    def _get_config_schema(self) -> str:
        """Get the JSON schema for MindRoom configuration."""
        output = ["# MindRoom Configuration Schema\n"]

        agent_schema = AgentConfig.model_json_schema()
        team_schema = TeamConfig.model_json_schema()

        output.append("## Agent Configuration Fields\n")
        output.append("```yaml")
        output.append("# Required fields:")
        for field, info in agent_schema.get("properties", {}).items():
            output.extend(self._format_schema_field(field, info, agent_schema.get("required", [])))
        output.append("```\n")

        output.append("## Team Configuration Fields\n")
        output.append("```yaml")
        for field, info in team_schema.get("properties", {}).items():
            output.extend(self._format_schema_field(field, info, team_schema.get("required", [])))
        output.append("```\n")

        output.append("## Team Modes")
        if "properties" in team_schema and "mode" in team_schema["properties"]:
            mode_info = team_schema["properties"]["mode"]
            if "enum" in mode_info:
                output.extend(f"- `{mode}`: {mode.title()} mode" for mode in mode_info["enum"])

        return "\n".join(output)

    def _get_mindroom_info(self) -> str:
        """Get comprehensive information about MindRoom."""
        docs = self._load_mindroom_docs()
        help_text = self._load_help_text()

        return f"""# MindRoom Documentation

## README Content:
{docs}

## Available Commands:
{help_text}

## Key Concepts:
- **Agents**: AI assistants with specific roles and tools
- **Teams**: Groups of agents that collaborate
- **Tools**: Integrations that give agents capabilities (80+ available)
- **Memory**: Persistent conversation memory across sessions
- **Threading**: Responders use explicit thread relations, and plain replies inherit thread membership transitively when their reply chain reaches a threaded ancestor
- **Routing**: Smart agent or team selection based on message content
- **Commands**: Special !commands for configuration and control
"""

    def _agent_entries_for_scope(
        self,
        config: Config,
        agent_scope: str,
    ) -> tuple[str, list[tuple[str, AgentConfig]]]:
        """Return the heading and agent entries for one listing scope."""
        all_agents = list(config.agents.items())
        if agent_scope == "all":
            return "Configured Agents", all_agents

        runtime_context = get_tool_runtime_context()
        if runtime_context is None:
            return "Configured Agents", all_agents

        room = runtime_context.room
        if room is None:
            return "Agents in This Room", []

        registry = entity_identity_registry(config, self.runtime_paths)
        available_responder_names = {
            agent_name
            for matrix_id in responder_candidate_entities_from_cached_room(
                room,
                runtime_context.requester_id,
                config,
                self.runtime_paths,
            )
            if (agent_name := registry.current_entity_name_for_user_id(matrix_id.full_id, include_router=False))
            is not None
        }
        agent_entries = [(name, agent) for name, agent in all_agents if name in available_responder_names]
        return "Agents in This Room", agent_entries

    def _list_agents(self, *, agent_scope: _AgentScope = "current_room") -> str:
        """List agents and their details."""
        config, load_error = self._load_config_or_error()
        if load_error:
            return load_error
        assert config is not None

        if agent_scope not in _VALID_AGENT_SCOPES:
            return "Error: agent_scope must be 'current_room' or 'all'."

        heading, agent_entries = self._agent_entries_for_scope(config, agent_scope)
        agents_info = []

        for name, agent in agent_entries:
            tools_str = ", ".join(agent.tool_names) if agent.tools else "No tools"
            role_line = f"  - Role: {agent.role[:100]}..." if len(agent.role) > 100 else f"  - Role: {agent.role}"
            agents_info.append(
                f"**{name}** ({agent.display_name})\n{role_line}\n  - Tools: {tools_str}\n  - Model: {agent.model}\n",
            )

        if not agents_info:
            if heading == "Agents in This Room":
                return "No agents are currently available in this room."
            return "No agents configured yet."

        return f"## {heading}:\n\n" + "\n".join(agents_info)

    def _list_teams(self) -> str:
        """List all configured teams and their composition."""
        config, load_error = self._load_config_or_error()
        if load_error:
            return load_error
        assert config is not None

        teams_info = []

        for name, team in config.teams.items():
            agents_str = ", ".join(team.agents)
            teams_info.append(
                f"**{name}** ({team.display_name})\n"
                f"  - Role: {team.role}\n"
                f"  - Agents: {agents_str}\n"
                f"  - Mode: {team.mode}\n",
            )

        if not teams_info:
            return "No teams configured yet."

        return "## Configured Teams:\n\n" + "\n".join(teams_info)

    def _list_available_tools(self) -> str:
        """List all available tools that can be used by agents."""
        _, tool_metadata, load_error = self._load_config_and_tool_metadata_or_error()
        if load_error:
            return load_error
        assert tool_metadata is not None

        tools_by_category: dict[str, list[tuple[str, str]]] = {}

        for tool_name in sorted(tool_metadata):
            metadata = tool_metadata[tool_name]
            category = metadata.category.value
            description = metadata.description
            if category not in tools_by_category:
                tools_by_category[category] = []
            tools_by_category[category].append((tool_name, description))

        output = ["## Available Tools by Category:\n"]
        for category in sorted(tools_by_category.keys()):
            output.append(f"\n### {category.title()}:")
            for tool_name, description in tools_by_category[category]:
                output.append(f"- **{tool_name}**: {description}")

        return "\n".join(output)

    def _get_tool_details(self, tool_name: str) -> str:
        """Get detailed information about a specific tool."""
        _, tool_metadata, load_error = self._load_config_and_tool_metadata_or_error()
        if load_error:
            return load_error
        assert tool_metadata is not None

        if tool_name not in tool_metadata:
            available = ", ".join(sorted(tool_metadata.keys()))
            return f"Unknown tool: {tool_name}\n\nAvailable tools: {available}"

        output = [f"## Tool: {tool_name}\n"]

        if tool_name in tool_metadata:
            metadata = tool_metadata[tool_name]
            output.append(f"**Display Name**: {metadata.display_name}")
            output.append(f"**Description**: {metadata.description}")
            output.append(f"**Category**: {metadata.category.value}")
            output.append(f"**Status**: {metadata.status.value}")
            output.append(f"**Setup Type**: {metadata.setup_type.value}")

            if metadata.config_fields:
                output.append("\n**Configuration Fields**:")
                for field in metadata.config_fields:
                    required = "Required" if field.required else "Optional"
                    output.append(f"- **{field.name}** ({field.type}, {required}): {field.description}")
                    if field.default is not None:
                        output.append(f"  Default: {field.default}")

            if metadata.dependencies:
                output.append(f"\n**Dependencies**: {', '.join(metadata.dependencies)}")

            if metadata.docs_url:
                output.append(f"\n**Documentation**: {metadata.docs_url}")
        else:
            output.append("No metadata available for this tool.")

        return "\n".join(output)

    def _create_agent_config(  # noqa: PLR0911
        self,
        agent_name: str,
        display_name: str,
        role: str,
        tools: list[str],
        instructions: list[str],
        model: str,
        rooms: list[str],
        knowledge_bases: list[str],
        include_default_tools: bool | None,
        markdown: bool | None,
        learning: bool | None,
        learning_mode: AgentLearningMode | None,
    ) -> str:
        """Create a new agent configuration."""
        # Validate agent name
        if not re.match(r"^[a-z0-9_]+$", agent_name):
            return "Error: Agent name must be lowercase alphanumeric with underscores only"

        config, tool_metadata, load_error = self._load_config_and_tool_metadata_or_error(
            footer=_CONFIG_CHANGE_REJECTED_MESSAGE,
        )
        if load_error:
            return load_error
        assert config is not None
        assert tool_metadata is not None

        invalid_tools = [t for t in tools if not _is_known_tool_entry(t, tool_metadata)]
        if invalid_tools:
            return f"Error: Unknown tools: {', '.join(invalid_tools)}\n\nUse get_info with info_type='available_tools' to see valid tools."

        try:
            if agent_name in config.agents:
                return f"Error: Agent '{agent_name}' already exists. Use manage_agent with operation='update' to modify it."

            knowledge_base_error = validate_knowledge_bases(knowledge_bases, set(config.knowledge_bases))
            if knowledge_base_error:
                return knowledge_base_error

            # Create new agent config
            new_agent = AgentConfig(
                display_name=display_name,
                role=role,
                tools=tools,  # ty: ignore[invalid-argument-type]
                instructions=instructions,
                model=model,
                rooms=rooms,
                knowledge_bases=knowledge_bases,
                include_default_tools=True if include_default_tools is None else include_default_tools,
                markdown=markdown,
                learning=learning,
                learning_mode=learning_mode,
            )

            # Add to config
            config.agents[agent_name] = new_agent

            # Save config
            persisted_config = validate_and_persist_config_payload(
                config.authored_model_dump(),
                self.runtime_paths,
            )

            oauth_guidance = _oauth_onboarding_guidance(
                persisted_config,
                self.runtime_paths,
                agent_name,
                [],
                persisted_config.resolve_entity(agent_name).available_tools,
                tool_metadata,
            )

            # Build success message
            tools_str = ", ".join(tools) if tools else "None"
            rooms_str = ", ".join(rooms) if rooms else "None"
            return (  # noqa: TRY300
                f"✅ Successfully created agent '{agent_name}'!\n\n"
                f"**Configuration:**\n"
                f"- Display Name: {display_name}\n"
                f"- Role: {role}\n"
                f"- Tools: {tools_str}\n"
                f"- Model: {model}\n"
                f"- Rooms: {rooms_str}\n\n"
                f"The agent is now available and can be mentioned with @{agent_name}"
                f"{oauth_guidance}"
            )
        except (ValidationError, ConfigRuntimeValidationError) as exc:
            return format_invalid_config_message(exc, footer=_CONFIG_CHANGE_REJECTED_MESSAGE)
        except Exception as e:
            logger.exception("Failed to create agent")
            return f"Error creating agent: {e}"

    def _update_agent_config(  # noqa: C901, PLR0911, PLR0912, PLR0915
        self,
        agent_name: str,
        display_name: str | None,
        role: str | None,
        tools: list[str] | None,
        instructions: list[str] | None,
        model: str | None,
        rooms: list[str] | None,
        knowledge_bases: list[str] | None,
        include_default_tools: bool | None,
        markdown: bool | None,
        learning: bool | None,
        learning_mode: AgentLearningMode | None,
    ) -> str:
        """Update an existing agent configuration."""
        config, tool_metadata, load_error = self._load_config_and_tool_metadata_or_error(
            footer=_CONFIG_CHANGE_REJECTED_MESSAGE,
        )
        if load_error:
            return load_error
        assert config is not None
        assert tool_metadata is not None

        try:
            if agent_name not in config.agents:
                return f"Error: Agent '{agent_name}' not found. Use manage_agent with operation='create' to create it."

            agent = config.agents[agent_name]
            previous_tools = config.resolve_entity(agent_name).available_tools

            # Validate tools if provided
            if tools is not None:
                invalid_tools = [t for t in tools if not _is_known_tool_entry(t, tool_metadata)]
                if invalid_tools:
                    return f"Error: Unknown tools: {', '.join(invalid_tools)}"

            if knowledge_bases is not None:
                knowledge_base_error = validate_knowledge_bases(knowledge_bases, set(config.knowledge_bases))
                if knowledge_base_error:
                    return knowledge_base_error

            changes = []

            if display_name is not None and display_name != agent.display_name:
                agent.display_name = display_name
                changes.append(f"Display Name -> {display_name}")

            if role is not None and role != agent.role:
                agent.role = role
                changes.append(f"Role -> {role}")

            if tools is not None and tools != agent.tool_names:
                agent.tools = preserve_tool_overrides(agent.tools, tools)
                changes.append(f"Tools -> {', '.join(tools) if tools else '(empty)'}")

            if instructions is not None and instructions != agent.instructions:
                agent.instructions = instructions
                if instructions:
                    changes.append(f"Instructions -> {len(instructions)} instructions")
                else:
                    changes.append("Instructions -> (empty)")

            if model is not None and model != agent.model:
                agent.model = model
                changes.append(f"Model -> {model}")

            if rooms is not None and rooms != agent.rooms:
                agent.rooms = rooms
                changes.append(f"Rooms -> {', '.join(rooms) if rooms else '(empty)'}")

            if knowledge_bases is not None and knowledge_bases != agent.knowledge_bases:
                agent.knowledge_bases = knowledge_bases
                changes.append(f"Knowledge Bases -> {', '.join(knowledge_bases) if knowledge_bases else '(empty)'}")

            if include_default_tools is not None and include_default_tools != agent.include_default_tools:
                agent.include_default_tools = include_default_tools
                changes.append(f"Include Default Tools -> {include_default_tools}")

            if markdown is not None and markdown != agent.markdown:
                agent.markdown = markdown
                changes.append(f"Markdown -> {markdown}")

            if learning is not None and learning != agent.learning:
                agent.learning = learning
                changes.append(f"Learning -> {learning}")

            if learning_mode is not None and learning_mode != agent.learning_mode:
                agent.learning_mode = learning_mode
                changes.append(f"Learning Mode -> {learning_mode}")

            if not changes:
                return "No changes made. All provided values are the same as current configuration."

            # Save config
            persisted_config = validate_and_persist_config_payload(
                config.authored_model_dump(),
                self.runtime_paths,
            )

            oauth_guidance = _oauth_onboarding_guidance(
                persisted_config,
                self.runtime_paths,
                agent_name,
                previous_tools,
                persisted_config.resolve_entity(agent_name).available_tools,
                tool_metadata,
            )

            return (
                f"✅ Successfully updated agent '{agent_name}'!\n\n**Changes:**\n"
                + "\n".join(f"- {c}" for c in changes)
                + oauth_guidance
            )
        except (ValidationError, ConfigRuntimeValidationError) as exc:
            return format_invalid_config_message(exc, footer=_CONFIG_CHANGE_REJECTED_MESSAGE)
        except Exception as e:
            logger.exception("Failed to update agent")
            return f"Error updating agent: {e}"

    def _create_team_config(  # noqa: PLR0911
        self,
        team_name: str,
        display_name: str,
        role: str,
        agents: list[str],
        mode: str = "coordinate",
    ) -> str:
        """Create a new team configuration."""
        if mode not in ["coordinate", "collaborate"]:
            return "Error: Team mode must be 'coordinate' or 'collaborate'"

        config, load_error = self._load_config_or_error(footer=_CONFIG_CHANGE_REJECTED_MESSAGE)
        if load_error:
            return load_error
        assert config is not None

        try:
            if team_name in config.teams:
                return f"Error: Team '{team_name}' already exists."

            # Validate agents exist
            invalid_agents = [a for a in agents if a not in config.agents]
            if invalid_agents:
                return f"Error: Unknown agents: {', '.join(invalid_agents)}"

            # Create new team config
            new_team = TeamConfig(
                display_name=display_name,
                role=role,
                agents=agents,
                mode=mode,
            )

            # Add to config
            config.teams[team_name] = new_team

            # Save config
            validate_and_persist_config_payload(config.authored_model_dump(), self.runtime_paths)

            return (
                f"✅ Successfully created team '{team_name}'!\n\n"
                f"**Configuration:**\n"
                f"- Display Name: {display_name}\n"
                f"- Role: {role}\n"
                f"- Agents: {', '.join(agents)}\n"
                f"- Mode: {mode}\n\n"
                f"The team can now be mentioned with @{team_name}"
            )
        except (ValidationError, ConfigRuntimeValidationError) as exc:
            return format_invalid_config_message(exc, footer=_CONFIG_CHANGE_REJECTED_MESSAGE)
        except Exception as e:
            logger.exception("Failed to create team")
            return f"Error creating team: {e}"

    def _validate_agent_config(self, agent_name: str) -> str:  # noqa: C901, PLR0912
        """Validate an agent's configuration."""
        config, tool_metadata, load_error = self._load_config_and_tool_metadata_or_error()
        if load_error:
            return load_error
        assert config is not None
        assert tool_metadata is not None

        if agent_name not in config.agents:
            return f"Error: Agent '{agent_name}' not found."

        agent = config.agents[agent_name]
        issues = []
        warnings = []

        # Check display name
        if not agent.display_name:
            issues.append("Missing display name")

        # Check role
        if not agent.role:
            warnings.append("No role description provided")
        elif len(agent.role) < 20:
            warnings.append("Role description is very short")

        # Check tools
        if not agent.tools:
            warnings.append("No tools configured")
        else:
            invalid_tools = [t for t in agent.tool_names if not _is_known_tool_entry(t, tool_metadata)]
            if invalid_tools:
                issues.append(f"Invalid tools: {', '.join(invalid_tools)}")

        # Check model
        available_models = list(config.models.keys()) if config.models else []
        if available_models and agent.model not in available_models:
            warnings.append(f"Model '{agent.model}' not in configured models: {', '.join(available_models)}")

        # Format results
        output = [f"## Validation Results for '{agent_name}':\n"]

        if not issues and not warnings:
            output.append("✅ Configuration is valid!")
        else:
            if issues:
                output.append("### ❌ Issues (must fix):")
                output.extend(f"- {issue}" for issue in issues)

            if warnings:
                output.append("\n### ⚠️ Warnings (consider fixing):")
                output.extend(f"- {warning}" for warning in warnings)

        # Add summary
        output.append("\n### Configuration Summary:")
        output.append(f"- Display Name: {agent.display_name}")
        output.append(f"- Role: {agent.role[:100]}..." if len(agent.role) > 100 else f"- Role: {agent.role}")
        output.append(f"- Tools: {', '.join(agent.tool_names) if agent.tools else 'None'}")
        output.append(f"- Model: {agent.model}")

        return "\n".join(output)

    def _get_agent_config(self, agent_name: str) -> str:
        """Get the full configuration for a specific agent."""
        config, load_error = self._load_config_or_error()
        if load_error:
            return load_error
        assert config is not None

        if agent_name not in config.agents:
            return f"Error: Agent '{agent_name}' not found."

        agent = config.agents[agent_name]
        agent_dict = agent.authored_model_dump()

        yaml_str = yaml.dump(agent_dict, default_flow_style=False, sort_keys=False)
        return f"## Configuration for '{agent_name}':\n\n```yaml\n{yaml_str}```"

    def _generate_agent_template(self, agent_type: str) -> str:
        """Generate a template configuration for common agent types."""
        _, tool_metadata, load_error = self._load_config_and_tool_metadata_or_error()
        if load_error:
            return load_error
        assert tool_metadata is not None

        # Map agent types to tool categories
        type_to_category = {
            "researcher": ToolCategory.RESEARCH,
            "developer": ToolCategory.DEVELOPMENT,
            "social": ToolCategory.SOCIAL,
            "communicator": ToolCategory.COMMUNICATION,
            "analyst": ToolCategory.INFORMATION,
            "productivity": ToolCategory.PRODUCTIVITY,
        }

        if agent_type not in type_to_category:
            available = ", ".join(type_to_category.keys())
            return f"Unknown template type: {agent_type}\n\nAvailable templates: {available}"

        category = type_to_category[agent_type]

        # Get tools from this category that are available
        tools = [
            name
            for name, metadata in tool_metadata.items()
            if metadata.category == category and metadata.status == ToolStatus.AVAILABLE
        ][:5]  # Limit to 5 tools

        # Generate role based on category
        role_descriptions = {
            ToolCategory.RESEARCH: "Research specialist focused on finding and analyzing information",
            ToolCategory.DEVELOPMENT: "Software development expert for coding and technical tasks",
            ToolCategory.SOCIAL: "Social interaction specialist for community engagement",
            ToolCategory.COMMUNICATION: "Communication expert for messaging and collaboration",
            ToolCategory.INFORMATION: "Information analyst for data processing and insights",
            ToolCategory.PRODUCTIVITY: "Productivity specialist for task and workflow management",
        }

        role = role_descriptions.get(category, f"Specialist in {category.value} tasks")

        # Generic instructions
        instructions = [
            f"Focus on {category.value} tasks",
            "Provide clear and actionable responses",
            "Use available tools effectively",
        ]

        return f"""## Template for '{agent_type}' agent:

```yaml
display_name: "{agent_type.title()} Agent"
role: "{role}"
tools: {yaml.dump(tools, default_flow_style=True).strip() if tools else "[]"}
instructions: {yaml.dump(instructions, default_flow_style=False).strip()}
model: "default"
```

**Available tools in {category.value} category:**
{chr(10).join(f"- {name}: {metadata.description}" for name, metadata in tool_metadata.items() if metadata.category == category)}

**To create this agent, use:**
```
manage_agent(
    operation="create",
    agent_name="{agent_type}_agent",
    display_name="{agent_type.title()} Agent",
    role="{role}",
    tools={tools},
    instructions={instructions},
)
```"""
