# MCP (Model Context Protocol) client
#
# Connects to MCP servers via socket transport (base R)

#' Connect to an MCP server
#'
#' Connects to an MCP server via TCP socket.
#'
#' @param host Character. Server hostname (default: "localhost").
#' @param port Integer. Server port.
#' @param name Character. Friendly name for this server.
#' @param timeout Numeric. Connection timeout in seconds (default: 30).
#'
#' @return An MCP connection object (list with socket and tools).
#' @export
#'
#' @examples
#' \dontrun{
#' # Start server first: r mcp_server.R --port 7850
#' conn <- mcp_connect(port = 7850, name = "codeR")
#' tools <- mcp_tools(conn)
#' result <- mcp_call(conn, "read_file", list(path = "README.md"))
#' mcp_close(conn)
#' }
mcp_connect <- function(host = "localhost", port, name = NULL, timeout = 30) {
    # Connect via socket
    sock <- tryCatch(
                     socketConnection(host = host, port = port, blocking = TRUE,
                                      open = "r+b", timeout = timeout),
                     error = function(e) {
        stop("Failed to connect to MCP server at ", host, ":", port,
             "\n  ", e$message, call. = FALSE)
    }
    )

    conn <- list(
                 socket = sock,
                 host = host,
                 port = port,
                 name = name %||% paste0(host, ":", port),
                 tools = list(),
                 request_id = 0L
    )
    class(conn) <- "mcp_connection"

    # Initialize handshake
    init_result <- .mcp_request(conn, "initialize", list(
            protocolVersion = "2024-11-05",
            capabilities = list(),
            clientInfo = list(name = "llm.api", version = "0.1.0")
        ))

    # Send initialized notification
    .mcp_notify(conn, "notifications/initialized", list())

    # Get tools
    tools_result <- .mcp_request(conn, "tools/list", list())
    conn$tools <- tools_result$tools

    conn
}

#' Start and connect to an MCP server
#'
#' Spawns an MCP server process and connects to it.
#' Requires the server script to support --port argument.
#'
#' @param command Character. Command to run the server (e.g., "r", "Rscript").
#' @param args Character vector. Arguments (path to server script).
#' @param port Integer. Port for the server (default: random 7850-7899).
#' @param name Character. Friendly name.
#' @param startup_wait Numeric. Seconds to wait for server startup.
#'
#' @return An MCP connection object.
#' @export
#' @examples
#' \dontrun{
#' conn <- mcp_start("Rscript", args = "mcp_server.R", port = 7850)
#' mcp_close(conn)
#' }
mcp_start <- function(command, args = character(), port = NULL, name = NULL,
                      startup_wait = 2) {
    # Pick random port if not specified
    if (is.null(port)) {
        port <- sample(7850:7899, 1)
    }

    # Add port argument
    full_args <- c(args, "--port", as.character(port))

    # Start server in background
    if (.Platform$OS.type == "windows") {
        system2(command, full_args, wait = FALSE, stdout = FALSE,
                stderr = FALSE)
    } else {
        system2(command, full_args, wait = FALSE,
                stdout = "/dev/null", stderr = "/dev/null")
    }

    # Wait for server to start
    Sys.sleep(startup_wait)

    # Connect
    mcp_connect(port = port, name = name %||% basename(args[1]))
}

#' List tools from an MCP connection
#'
#' @param conn An MCP connection object.
#' @return List of tool definitions.
#' @export
#' @examples
#' \dontrun{
#' conn <- mcp_connect(port = 7850)
#' tools <- mcp_tools(conn)
#' mcp_close(conn)
#' }
mcp_tools <- function(conn) {
    conn$tools
}

#' Call a tool on an MCP server
#'
#' @param conn An MCP connection object.
#' @param name Character. Tool name.
#' @param arguments List. Tool arguments.
#'
#' @return Tool result (list with content and text).
#' @export
#' @examples
#' \dontrun{
#' conn <- mcp_connect(port = 7850)
#' result <- mcp_call(conn, "read_file", list(path = "README.md"))
#' mcp_close(conn)
#' }
mcp_call <- function(conn, name, arguments = list()) {
    result <- .mcp_request(conn, "tools/call",
                           list(name = name, arguments = arguments))

    # Extract text content
    if (!is.null(result$content)) {
        texts <- vapply(result$content, function(c) {
            if (identical(c$type, "text")) c$text else ""
        }, character(1))
        result$text <- paste(texts, collapse = "\n")
    }

    result
}

#' Close an MCP connection
#'
#' @param conn An MCP connection object.
#' @return \code{NULL}, invisibly. Called for its side effect of closing
#'   the underlying socket.
#' @export
#' @examples
#' \dontrun{
#' conn <- mcp_connect(port = 7850)
#' mcp_close(conn)
#' }
mcp_close <- function(conn) {
    tryCatch(close(conn$socket), error = function(e) NULL)
    invisible(NULL)
}

#' Format MCP tools for LLM APIs
#'
#' Converts MCP tool definitions to the format used by Claude/OpenAI.
#'
#' @param conn An MCP connection, or list of connections.
#' @return List of tools in API format.
#' @export
#' @examples
#' \dontrun{
#' conn <- mcp_connect(port = 7850)
#' tools <- mcp_tools_for_api(conn)
#' mcp_close(conn)
#' }
mcp_tools_for_api <- function(conn) {
    if (inherits(conn, "mcp_connection")) {
        conns <- list(conn)
    } else {
        conns <- conn
    }

    tools <- list()
    for (c in conns) {
        for (tool in c$tools) {
            tools[[length(tools) + 1]] <- list(name = tool$name,
                description = tool$description %||% "",
                input_schema = tool$inputSchema)
        }
    }
    tools
}

#' Format MCP tools for Claude API
#'
#' Wrapper for \code{\link{mcp_tools_for_api}}, retained for backwards
#' compatibility.
#'
#' @param conn An MCP connection, or list of connections.
#' @return List of tools in API format.
#' @export
#' @examples
#' \dontrun{
#' conn <- mcp_connect(host = "localhost", port = 7850)
#' tools <- mcp_tools_for_claude(conn)
#' mcp_close(conn)
#' }
mcp_tools_for_claude <- function(conn) {
    mcp_tools_for_api(conn)
}

# Internal: send JSON-RPC request and get response
.mcp_request <- function(conn, method, params) {
    conn$request_id <- conn$request_id + 1L
    id <- conn$request_id

    request <- list(jsonrpc = "2.0", id = id, method = method, params = params)

    json <- jsonlite::toJSON(request, auto_unbox = TRUE, null = "null")

    # Write request
    writeLines(json, conn$socket)

    # Read response
    response_line <- readLines(conn$socket, n = 1, warn = FALSE)

    if (length(response_line) == 0 || nchar(response_line) == 0) {
        stop("MCP server closed connection", call. = FALSE)
    }

    response <- jsonlite::fromJSON(response_line, simplifyVector = FALSE)

    if (!is.null(response$error)) {
        stop("MCP error: ", response$error$message, call. = FALSE)
    }

    response$result
}

# Internal: send notification (no response expected)
.mcp_notify <- function(conn, method, params) {
    request <- list(jsonrpc = "2.0", method = method, params = params)

    json <- jsonlite::toJSON(request, auto_unbox = TRUE, null = "null")
    writeLines(json, conn$socket)
    invisible(NULL)
}

#' Print an MCP Connection
#'
#' S3 print method for MCP connection objects.
#'
#' @param x An MCP connection object.
#' @param ... Unused.
#' @return \code{x}, invisibly. Called for the side effect of printing a
#'   summary of the connection state and available tools.
#' @export
#' @examples
#' \dontrun{
#' conn <- mcp_connect(port = 7850)
#' print(conn)
#' mcp_close(conn)
#' }
print.mcp_connection <- function(x, ...) {
    status <- tryCatch({
        if (isOpen(x$socket)) "connected" else "disconnected"
    }, error = function(e) "disconnected")

    cat(sprintf("MCP Connection: %s (%s)\n", x$name, status))
    cat(sprintf("  Endpoint: %s:%d\n", x$host, x$port))
    cat(sprintf("  Tools: %d\n", length(x$tools)))
    if (length(x$tools) > 0) {
        for (tool in x$tools) {
            cat(sprintf("    - %s\n", tool$name))
        }
    }
    invisible(x)
}

