"""One catalog for protocol schemas, profiles, and documentation."""

from __future__ import annotations

from copy import deepcopy

from mcp.types import Tool, ToolAnnotations

INSTRUCTIONS = (
    "If these tools are deferred (for example in Claude Code), load them with ToolSearch, for example "
    "'attocode-code-intel inspect_symbol'. For a known symbol, call inspect_symbol. For unfamiliar behavior, "
    "call bootstrap with a task_hint. Use search_symbols to find names, and impact_analysis and suggest_tests "
    "to plan and check a change. Grep stays useful for literal text. Cite each excerpt range separately. "
    "An incomplete index does not prove that a dependency is absent. {watch} Keep uncommitted code local."
)
# ponytail: two variants of one sentence; the server knows whether it watches files.
WATCHING = "The server watches the project, so it indexes your edits without a call."
NOT_WATCHING = "This server does not watch files: after edits, call notify_file_changed."
INSTRUCTIONS, INSTRUCTIONS_NO_WATCH = INSTRUCTIONS.format(watch=WATCHING), INSTRUCTIONS.format(watch=NOT_WATCHING)
PROFILES = ("daily", "full", "lean")
COMPACT_PROFILES = frozenset({"daily", "lean"})
LEAN = frozenset({"bootstrap", "search_symbols", "inspect_symbol", "semantic_search",
                  "impact_analysis", "suggest_tests", "capabilities"})
LEAN_INSTRUCTIONS = (
    "Code intelligence is optional; use native tools whenever useful. If you choose this server, "
    "inspect_symbol handles a known symbol, search_symbols finds names, and bootstrap or semantic_search "
    "locates unfamiliar behavior. impact_analysis and suggest_tests offer candidates, not complete coverage. "
    "Cite each source range separately. Missing relationships do not prove absence. {watch} "
    "Advanced operations require a new full-profile session; ToolSearch cannot reveal unadvertised tools. "
    "Keep uncommitted code local."
)


def profile_instructions(profile="daily", *, watch=True):
    if profile == "lean":
        return LEAN_INSTRUCTIONS.format(watch=WATCHING if watch else NOT_WATCHING)
    return INSTRUCTIONS if watch else INSTRUCTIONS_NO_WATCH


DAILY = frozenset(
    {
        "bootstrap",
        "project_summary",
        "explore_codebase",
        "search_symbols",
        "inspect_symbol",
        "semantic_search",
        "fast_search",
        "symbols",
        "relevant_context",
        "cross_references",
        "dependencies",
        "call_graph",
        "impact_analysis",
        "suggest_tests",
        "review_change",
        "notify_file_changed",
        "hydration_status",
        "recall",
        "record_learning",
        "list_learnings",
        "learning_feedback",
    }
)
# Remote daily keeps the learnings for team knowledge. A local daily session leaves them out:
# their schemas cost about 1,600 tokens in every model request.
LEARNING = frozenset({"recall", "record_learning", "list_learnings", "learning_feedback", "update_learning"})
WRITES = frozenset(
    [
        "notify_file_changed",
        "record_adr",
        "update_adr_status",
        "pin_current",
        "pin_delete",
        "track_file_access",
        "clear_frecency",
        "record_learning",
        "learning_feedback",
        "update_learning",
        "reindex",
        "track_query_result",
        "clear_query_history",
        "migrate_cache",
        "clear_symbols",
        "clear_embeddings",
        "clear_trigrams",
        "clear_kw_index",
        "clear_learnings",
        "clear_adrs",
        "clear_all",
        "cas_clear",
        "export_learnings",
        "import_learnings",
        "export_adrs_markdown",
        "import_adrs_markdown",
        "gc_run",
        "embeddings_rotate_start",
        "embeddings_rotate_step",
        "embeddings_rotate_cutover",
        "embeddings_rotate_gc_old",
        "embeddings_rotate_abort",
        "snapshot_create",
        "snapshot_delete",
        "snapshot_restore",
        "overlay_create",
        "overlay_activate",
        "overlay_delete",
        "install_pack",
        "register_rule",
        "rule_feedback",
        "evolve_rules",
        "synthesize_rule",
        "rule_hygiene",
        "install_community_pack",
        "import_rules",
    ]
)
REMOTE_WRITES = frozenset(
    {"record_learning", "learning_feedback", "record_adr", "update_adr_status", "update_learning"}
)


def compact_schema(node, names=False):
    """The input schema that a model reads: no pydantic titles, and no null branch for an optional argument.

    NAMES marks a map of property names, whose keys are not schema keywords. Validation keeps the full
    schema, so an explicit null stays valid.
    """
    if isinstance(node, list):
        return [compact_schema(item) for item in node]
    if not isinstance(node, dict):
        return node
    if names:
        return {key: compact_schema(value) for key, value in node.items()}
    node = {key: compact_schema(value, key in ("properties", "$defs", "definitions"))
            for key, value in node.items() if key != "title"}
    options = node.get("anyOf")
    if (isinstance(options, list) and len(options) == 2 and {"type": "null"} in options
            and "default" in node and node["default"] is None):
        del node["anyOf"], node["default"]
        node.update(next(option for option in options if option != {"type": "null"}))
    return node


def remote_available(name):
    return name not in {"change_context", "trace_flow"} and (not is_write(name) or name in REMOTE_WRITES)


def registered_tools():
    from attocode_intel.server import mcp

    return mcp._tool_manager._tools


def is_write(name: str) -> bool:
    return name in WRITES


def tool_catalog(profile: str = "full", *, remote: bool = False) -> list[Tool]:
    if profile not in PROFILES:
        raise ValueError("profile must be daily, full or lean")
    if remote and profile == "lean":
        raise ValueError("lean is an experimental local profile; remote profiles are daily or full")
    result = []
    for name, tool in registered_tools().items():
        if profile == "daily" and name not in DAILY:
            continue
        if remote and not remote_available(name):
            continue
        schema = deepcopy(tool.parameters)
        schema.setdefault("properties", {}).update(
            {
                "workspace": {
                    "type": "string",
                    "description": "Local project path or authorized remote repository ID.",
                },
                "revision": {
                    "type": "string",
                    "description": "Remote branch or commit; omitted uses the default branch.",
                },
            }
        )
        schema["properties"]["max_tokens"] = {
            "type": "integer",
            "minimum": 1,
            "maximum": 32000,
            "default": 8000,
            "description": "Maximum tokens in the complete readable response, including provenance.",
        }
        result.append(
            Tool(
                name=name,
                description=tool.description,
                inputSchema=schema,
                annotations=ToolAnnotations(
                    readOnlyHint=not is_write(name),
                    destructiveHint=name.startswith(("clear_", "delete_"))
                    or name
                    in {"snapshot_delete", "snapshot_restore", "overlay_delete", "pin_delete"},
                ),
            )
        )
    result.extend(
        [
            Tool(
                name="update_learning",
                description="Edit or archive a repository learning.",
                inputSchema={
                    "type": "object",
                    "required": ["learning_id"],
                    "properties": {
                        "learning_id": {"type": "integer"},
                        "description": {"type": "string"},
                        "details": {"type": "string"},
                        "status": {"type": "string", "enum": ["active", "archived"]},
                        "confidence": {"type": "number", "minimum": 0, "maximum": 1},
                        "workspace": {"type": "string"},
                        "revision": {"type": "string"},
                    },
                },
                annotations=ToolAnnotations(readOnlyHint=False),
            ),
            Tool(
                name="cross_repo_search",
                description="Search explicitly selected authorized repositories; results retain repository and commit provenance.",
                inputSchema={
                    "type": "object",
                    "required": ["query", "workspaces"],
                    "properties": {
                        "query": {"type": "string"},
                        "workspaces": {
                            "type": "array",
                            "items": {"type": "string"},
                            "minItems": 1,
                            "maxItems": 20,
                        },
                        "top_k": {"type": "integer", "minimum": 1, "maximum": 50},
                    },
                },
                annotations=ToolAnnotations(readOnlyHint=True),
            ),
        ]
    )
    result.append(
        Tool(
            name="capabilities",
            description="Available tools, profiles, workspace selection, and optional enhancements.",
            inputSchema={
                "type": "object",
                "properties": {"workspace": {"type": "string"}, "revision": {"type": "string"}},
            },
            annotations=ToolAnnotations(readOnlyHint=True),
        )
    )
    # Apply common arguments to synthetic tools too (knowledge and cross-repo search).
    for tool in result:
        tool.inputSchema.setdefault("properties", {}).setdefault("max_tokens", {
            "type": "integer", "minimum": 1, "maximum": 32000, "default": 8000,
            "description": "Readable response budget; daily MCP budgets the entire serialized result.",
        })
        if not remote:  # local analysis always reads the working tree
            tool.inputSchema["properties"].pop("revision", None)
    local_daily = profile == "daily" and not remote
    return [tool for tool in result
            if not (local_daily and tool.name in LEARNING)
            and (profile != "lean" or tool.name in LEAN | {"notify_file_changed"})]
