//! MCP connection helper shared by `tools call` and `mcptest pipe`.
//!
//! Both new surfaces talk to the same kind of target the rest of the CLI
//! does: a streamable HTTP MCP endpoint reached by URL, optionally carrying a
//! bearer token. Rather than duplicate the transport-and-handshake dance in
//! two handlers, this module owns it. Multi-server pipelines layer
//! on top of this single-target connector in a follow-up; v0 is URL-first to
//! match the imperative `tools` surface.

use std::time::Duration;

use anyhow::{Context, Result};
use mcptest_core::protocol::{Client, ProtocolVersion};
use mcptest_core::transport::{StreamableHttp, StreamableHttpConfig};
use serde_json::{json, Value};

/// A connected, initialized MCP client plus its transport handle.
///
/// The handle must be shut down after use; [`Connection::close`] does both
/// so callers do not leak the SSE reader task.
pub struct Connection {
    client: Client,
    handle: StreamableHttp,
}

impl Connection {
    /// Connect to `url`, run the `initialize` handshake, and return a ready
    /// client. `bearer_token` is attached to every request when present.
    pub async fn connect(url: &str, bearer_token: Option<String>) -> Result<Self> {
        let config = StreamableHttpConfig {
            url: url.to_string(),
            bearer_token,
            request_timeout: Duration::from_secs(30),
            connect_timeout: Duration::from_secs(15),
            ..StreamableHttpConfig::default()
        };
        let (transport, handle) = StreamableHttp::connect(config)
            .map_err(|e| anyhow::anyhow!("streamable HTTP setup failed: {e}"))?;
        let client = Client::builder(transport)
            .with_request_timeout(Duration::from_secs(30))
            .build();
        client
            .initialize(ProtocolVersion::current())
            .await
            .context("initialize handshake failed")?;
        Ok(Self { client, handle })
    }

    /// Call one tool with the given arguments, returning the raw `result`
    /// payload from the `tools/call` response.
    pub async fn call_tool(&self, tool: &str, args: &Value) -> Result<Value> {
        let params = json!({ "name": tool, "arguments": args });
        self.client
            .request("tools/call", params)
            .await
            .map_err(|e| anyhow::anyhow!("tools/call `{tool}` failed: {e}"))
    }

    /// Close the client and shut down the transport reader task.
    pub async fn close(self) {
        self.client.close().await.ok();
        self.handle.shutdown().await;
    }
}
