"""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.
"""

from mcp.types import ToolAnnotations

from .server import fetch_data, logger, mcp

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


# ═══════════════════════════════════════════════════════════════════════════
#  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.

    Args:
        node_name: Filter by node name (e.g. DOMAIN1)
        node_type: Filter by node type (e.g. DOMAIN, PGM, SRVPGM, DATA)
        node_id:   Filter by numeric node ID
    """
    logger.info("Tool: list_nodes(name=%s, type=%s, id=%s)", node_name, node_type, node_id)
    return await fetch_data(
        "listNodes", nodeName=node_name, nodeType=node_type, nodeId=node_id
    )


@mcp.tool(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 recursively used by the given program (top-down call chain).

    Provide either node_name or node_id to identify the starting program.

    Args:
        node_name: Program name (e.g. LOM710RP, SL001S)
        node_type: Node type filter (e.g. PGM, DATA)
        node_id:   Numeric node ID
    """
    logger.info("Tool: topdown(name=%s, type=%s, id=%s)", node_name, node_type, node_id)
    return await fetch_data(
        "topdown", nodeName=node_name, nodeType=node_type, nodeId=node_id
    )


@mcp.tool(annotations=_READ_ONLY)
async def topdown_prompt(
    node_name: str | None = None,
    node_type: str | None = None,
    node_id: int | None = None,
) -> str:
    """Get a top-down call chain with description and source intro for each node.

    Similar to topdown but includes descriptive text suitable for prompts.

    Args:
        node_name: Program name (e.g. LOM710RP)
        node_type: Node type filter (e.g. PGM, DATA)
        node_id:   Numeric node ID
    """
    logger.info("Tool: topdown_prompt(name=%s, type=%s, id=%s)", node_name, node_type, node_id)
    return await fetch_data(
        "topdownPrompt", nodeName=node_name, nodeType=node_type, nodeId=node_id
    )


@mcp.tool(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.

    Args:
        node_name: Program name (e.g. LOM710RP)
        node_id:   Numeric node ID
    """
    logger.info("Tool: bottomup(name=%s, id=%s)", node_name, node_id)
    return await fetch_data("bottomup", nodeName=node_name, nodeId=node_id)


@mcp.tool(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
    """
    logger.info("Tool: prompt(name=%s, id=%s)", node_name, node_id)
    return await fetch_data("prompt", nodeName=node_name, nodeId=node_id)


@mcp.tool(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.

    Returns node details plus source file information (library, file, member,
    type, timestamps, and the source text).

    Args:
        node_name: Program or table name (e.g. LOM710RP)
        node_id:   Numeric node ID
    """
    logger.info("Tool: load_source(name=%s, id=%s)", node_name, node_id)
    return await fetch_data("loadSource", nodeName=node_name, nodeId=node_id)


@mcp.tool(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
    """
    logger.info("Tool: search_source(search=%s)", search)
    return await fetch_data("searchSource", 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.
    """
    logger.info("Tool: list_errors()")
    return await fetch_data("listError")


@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.
    """
    logger.info("Tool: get_domains()")
    return await fetch_data("getDomains")


@mcp.tool(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
    """
    logger.info("Tool: get_markdown(name=%s, type=%s, id=%s)", node_name, node_type, node_id)
    return await fetch_data(
        "markdown", nodeName=node_name, nodeType=node_type, nodeId=node_id
    )


@mcp.tool(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
    """
    logger.info("Tool: get_html(name=%s, type=%s, id=%s)", node_name, node_type, node_id)
    return await fetch_data(
        "html", nodeName=node_name, nodeType=node_type, nodeId=node_id
    )


# ═══════════════════════════════════════════════════════════════════════════
#  Mutation tools (require user approval in MCP clients)
# ═══════════════════════════════════════════════════════════════════════════


@mcp.tool(annotations=_MUTATING)
async def set_domain(node_id: int, domain_node_id: int) -> str:
    """Assign a domain to a node.

    Removes any existing domain edge for the node, updates the node's
    domain_node_id, and creates a new DOMAIN edge.

    Args:
        node_id:        The ID of the node to assign
        domain_node_id: The ID of the domain node to assign it to
    """
    logger.info("Tool: set_domain(node_id=%s, domain_node_id=%s)", node_id, domain_node_id)
    return await fetch_data(
        "setDomain", nodeId=node_id, domainNodeId=domain_node_id
    )


@mcp.tool(annotations=_ADDITIVE)
async def create_domain(node_name: str, description: str) -> str:
    """Create a new domain.

    Inserts a new DOMAIN node with the given name and description.
    Spaces are stripped from the name.

    Args:
        node_name:   Name for the new domain (spaces will be removed)
        description: Human-readable description of the domain
    """
    logger.info("Tool: create_domain(name=%s)", node_name)
    return await fetch_data(
        "createDomain", nodeName=node_name, description=description
    )
