"""MCP tool definitions for the Sitemule Blueprint API.

Each tool corresponds to a procedure in the RPGLE backend (api.rpgle).
Read-only tools are annotated accordingly; mutation tools are marked as
destructive so that MCP clients can prompt the user for approval.
"""

import json

from mcp.types import ToolAnnotations

from .server import (
    _compact_sources,
    _html_to_text,
    _strip_envelope,
    _strip_nulls,
    fetch_data,
    fetch_field,
    graph_view_uri,
    logger,
    mcp,
    settings,
)

# ---------------------------------------------------------------------------
# Annotation shortcuts
# ---------------------------------------------------------------------------
_READ_ONLY = ToolAnnotations(readOnlyHint=True, destructiveHint=False)
_MUTATING = ToolAnnotations(readOnlyHint=False, destructiveHint=True)
_ADDITIVE = ToolAnnotations(readOnlyHint=False, destructiveHint=False)

# Fields returned by get_program_info (metadata only — no source text)
_INFO_FIELDS: set[str] = {
    "id",
    "nodeName",
    "nodeType",
    "libraryName",
    "objectName",
    "objectType",
    "objectAttribute",
    "description",
    "info",
    "sourceChanged",
    "domainNodeId",
}


# ═══════════════════════════════════════════════════════════════════════════
#  Read / Query tools
# ═══════════════════════════════════════════════════════════════════════════


@mcp.tool(annotations=_READ_ONLY)
async def list_nodes(
    node_name: str | None = None,
    node_type: str | None = None,
    node_id: int | None = None,
) -> str:
    """List nodes from the Blueprint graph, optionally filtered.

    Currently returns domain-level nodes.

    The result includes a `[↗ Open graph view in VS Code](vscode://...)` markdown link.
    Always include that link verbatim in your response so the user can click it directly.
    Do NOT open it via xdg-open, a terminal command, or any shell execution.

    Args:
        node_name: Filter by node name (e.g. POSFORRA)
        node_type: Filter by node type (e.g. DOMAIN, PGM, SRVPGM, DATA), can be blank to include all types
        node_id:   Filter by numeric node ID
    """
    result = await fetch_data(
        "listNodes", _label="list_nodes",
        nodeName=node_name, nodeType=node_type, nodeId=node_id,
    )
    uri = graph_view_uri("listNodes", nodeName=node_name, nodeType=node_type, nodeId=node_id)
    return result + f"\n\n---\n[↗ Open graph view in VS Code]({uri})"


@mcp.tool(name="get_nodes_used_by", annotations=_READ_ONLY)
async def topdown(
    node_name: str | None = None,
    node_type: str | None = None,
    node_id: int | None = None,
) -> str:
    """Get all programs and data-entities recursively used by the given node (top-down call chain).

    Provide either node_name or node_id to identify the starting node.

    The result includes a `[↗ Open graph view in VS Code](vscode://...)` markdown link.
    Always include that link verbatim in your response so the user can click it directly.
    Do NOT open it via xdg-open, a terminal command, or any shell execution.

    Args:
        node_name: Program name (e.g. LOM710RP, SL001S)
        node_type: Node type filter (e.g. PGM, DATA)
        node_id:   Numeric node ID
    """
    result = await fetch_data(
        "topdown", _label="topdown",
        nodeName=node_name, nodeType=node_type, nodeId=node_id,
    )
    uri = graph_view_uri("topdown", nodeName=node_name, nodeType=node_type, nodeId=node_id)
    return result + f"\n\n---\n[↗ Open graph view in VS Code]({uri})"


@mcp.tool(name="get_program_callees", annotations=_READ_ONLY)
async def bottomup(
    node_name: str | None = None,
    node_id: int | None = None,
) -> str:
    """See where a program is called from (bottom-up / reverse call chain).

    Provide either node_name or node_id.

    The result includes a `[↗ Open graph view in VS Code](vscode://...)` markdown link.
    Always include that link verbatim in your response so the user can click it directly.
    Do NOT open it via xdg-open, a terminal command, or any shell execution.

    Args:
        node_name: Program name (e.g. LOM710RP)
        node_id:   Numeric node ID
    """
    result = await fetch_data("bottomup", _label="bottomup", nodeName=node_name, nodeId=node_id)
    uri = graph_view_uri("bottomup", nodeName=node_name, nodeId=node_id)
    return result + f"\n\n---\n[↗ Open graph view in VS Code]({uri})"


# Disable tool for the time being.
# It's not clear how it's different from get_program_info or get_program_source,
# and it may be too expensive to run as a default prompt
#@mcp.tool(name="get_program_description_and_partial_source", annotations=_READ_ONLY)
async def prompt(
    node_name: str | None = None,
    node_id: int | None = None,
) -> str:
    """Get descriptions and a source-code intro for a program and its call chain.

    Returns distinct nodes with their description and the first 2000 chars of source.

    Args:
        node_name: Program name (e.g. LOM710RP)
        node_id:   Numeric node ID
    """
    return await fetch_data("prompt", _label="prompt", nodeName=node_name, nodeId=node_id)


@mcp.tool(annotations=_READ_ONLY)
async def get_program_info(
    node_name: str | None = None,
    node_id: int | None = None,
) -> str:
    """Get metadata and description for a program or table — no source code.

    Returns node identity, type, library, description, and the HTML info
    page (rendered as plain text in compact mode).  Use get_program_source
    when you need the actual source text.

    Args:
        node_name: Program or table name (e.g. LOM710RP)
        node_id:   Numeric node ID
    """
    return await fetch_field(
        "loadSource",
        include=_INFO_FIELDS,
        _label="get_program_info",
        nodeName=node_name,
        nodeId=node_id,
    )


@mcp.tool(annotations=_READ_ONLY)
async def get_program_source(
    node_name: str | None = None,
    node_id: int | None = None,
) -> str:
    """Get the source code text for a program or table.

    Returns the source text plus a deduplicated list of source-file locations
    (library, file, member, timestamps). In compact mode the sources list is
    collapsed to the most-recent entry with ``alsoIn`` listing other libraries.

    Args:
        node_name: Program or table name (e.g. LOM710RP)
        node_id:   Numeric node ID
    """
    raw = await fetch_field(
        "loadSource",
        include={"source", "sources"},
        _label="get_program_source",
        nodeName=node_name,
        nodeId=node_id,
    )
    # In compact mode, apply sources deduplication on the already-parsed data
    if settings.compact_responses:
        try:
            data = json.loads(raw)
            if isinstance(data, dict) and "sources" in data:
                data["sources"] = _compact_sources(data["sources"])
            raw = json.dumps(_strip_nulls(data), separators=(",", ":"))
        except Exception:
            pass
    return raw


@mcp.tool(name="get_program_source_code_and_info", annotations=_READ_ONLY)
async def load_source(
    node_name: str | None = None,
    node_id: int | None = None,
) -> str:
    """Get full source code **and** metadata for a program or table.

    Combines everything from get_program_info and get_program_source in one
    call.  Prefer the focused tools when you only need one part.

    Args:
        node_name: Program or table name (e.g. LOM710RP)
        node_id:   Numeric node ID
    """
    return await fetch_data("loadSource", _label="load_source", nodeName=node_name, nodeId=node_id)


@mcp.tool(name="search_all_source_code_with_regex", annotations=_READ_ONLY)
async def search_source(search: str) -> str:
    """Search the source repository for text matching a regular expression.

    Returns matching nodes with their name, type, library, and object info.

    Args:
        search: Regular expression to search for in the source code
    """
    return await fetch_data("searchSource", _label="search_source", nodeName=search)


@mcp.tool(annotations=_READ_ONLY)
async def list_errors() -> str:
    """List entries from the Blueprint error log.

    Returns error log records with timestamps, types, and descriptions.
    """
    return await fetch_data("listError", _label="list_errors")


@mcp.tool(annotations=_READ_ONLY)
async def get_domains() -> str:
    """Get all defined domains.

    Returns a list of domain nodes with an additional \"No domain\" entry
    for unassigned nodes.

    The result includes a `[↗ Open graph view in VS Code](vscode://...)` markdown link.
    Always include that link verbatim in your response so the user can click it directly.
    Do NOT open it via xdg-open, a terminal command, or any shell execution.
    """
    result = await fetch_data("getDomains", _label="get_domains")
    uri = graph_view_uri("listNodes", nodeType="DOMAIN")
    return result + f"\n\n---\n[↗ Open graph view in VS Code]({uri})"


@mcp.tool(name="get_program_markdown", annotations=_READ_ONLY)
async def get_markdown(
    node_name: str | None = None,
    node_type: str | None = None,
    node_id: int | None = None,
) -> str:
    """Get Markdown documentation for a program or node.

    Generates a top-down Markdown report.

    Args:
        node_name: Program name (e.g. LAGFORRA, LOM710RP)
        node_type: Node type filter (e.g. PGM, DATA)
        node_id:   Numeric node ID
    """
    return await fetch_data(
        "markdown", _label="get_markdown",
        nodeName=node_name, nodeType=node_type, nodeId=node_id,
    )


@mcp.tool(name="get_program_documentation", annotations=_READ_ONLY)
async def get_html(
    node_name: str | None = None,
    node_type: str | None = None,
    node_id: int | None = None,
) -> str:
    """Get HTML documentation for a program or node.

    Generates a top-down HTML report.

    Args:
        node_name: Program name (e.g. LAGFORRA, LOM710RP)
        node_type: Node type filter (e.g. PGM, DATA)
        node_id:   Numeric node ID
    """
    return await fetch_data(
        "html", _label="get_html",
        nodeName=node_name, nodeType=node_type, nodeId=node_id,
    )