///| Legacy client for the 2025-11-25 protocol era.
///
/// The modern `MCPClient` speaks the stateless 2026-07-28 protocol: no
/// initialize handshake, per-request `_meta`. When a deployment must talk to
/// an older server that still expects the legacy `initialize`/session flow,
/// it can probe the server's era first (modern `server/discover` succeeds →
/// modern; non-modern error/timeout → legacy) and fall back to this client.
///
/// `LegacyClient` owns the legacy `initialize` + `notifications/initialized`
/// handshake and the session-aware transport concerns that the modern client
/// has shed. It depends only on `transport` and `protocol/types` — it never
/// imports the modern `client` package, so deleting this directory when the
/// ecosystem has migrated is a clean cut.
///
/// Scope of this module: the handshake and a minimal request send/receive
/// loop. Era probing and ClientBackend dispatch in the modern client are the
/// integration layer that decides when to construct a LegacyClient; that
/// integration is tracked as a follow-up.

///|
/// Protocol version this legacy client advertises during initialize.
let legacy_protocol_version : String = "2025-11-25"

///|
pub struct LegacyClient {
  client_name : String
  client_version : String
  transport : LegacyTransport
  mut next_id : Int
  mut server_name : String
  mut server_version : String
}

///|
pub fn LegacyClient::LegacyClient(
  name~ : String,
  version~ : String,
  transport~ : @transport.AnyTransport,
) -> LegacyClient {
  {
    client_name: name,
    client_version: version,
    transport: LegacyTransport::Wrapped(transport),
    next_id: 0,
    server_name: "unknown",
    server_version: "unknown",
  }
}

///|
fn LegacyClient::next_request_id(self : LegacyClient) -> Int {
  self.next_id = self.next_id + 1
  self.next_id
}

///|
/// Parse the `InitializeResult` response into `ServerInfo`.
fn parse_initialize_response(
  response : String,
) -> Result[@types.ServerInfo, @types.MCPError] {
  let json = @json.parse(response) catch {
    _ => return Err(@types.ParseError("Invalid initialize response"))
  }
  match json {
    Object(obj) =>
      match obj.get("result") {
        Some(Object(result)) =>
          match result.get("serverInfo") {
            Some(Object(info)) => {
              let name = match info.get("name") {
                Some(String(n)) => n
                _ => "unknown"
              }
              let version = match info.get("version") {
                Some(String(v)) => v
                _ => "unknown"
              }
              Ok({ name, title: None, version, description: None })
            }
            _ =>
              Ok({
                name: "unknown",
                title: None,
                version: "unknown",
                description: None,
              })
          }
        _ => Err(@types.InvalidRequest("initialize response missing result"))
      }
    _ => Err(@types.ParseError("initialize response is not an object"))
  }
}

///|
/// Perform the legacy initialize handshake: send `initialize`, parse
/// `serverInfo`/`protocolVersion` from the response, then send
/// `notifications/initialized`. Returns the server identity on success.
///
/// The legacy client advertises empty client capabilities: it has no inbound
/// request handlers (roots/listChanged and sampling are provided by the modern
/// client after the 2026-07-28 migration), so claiming them here would be a lie.
pub async fn LegacyClient::initialize(
  self : LegacyClient,
) -> Result[@types.ServerInfo, @types.MCPError] {
  let id = self.next_request_id()
  // Build the initialize request body for the 2025-11-25 shape.
  // Capabilities are intentionally empty: this fallback client cannot serve
  // inbound roots/listChanged or sampling requests.
  let caps : Map[String, Json] = Map([])
  let request = Json::object({
    "jsonrpc": Json::string("2.0"),
    "id": Json::number(id.to_double()),
    "method": Json::string("initialize"),
    "params": Json::object({
      "protocolVersion": Json::string(legacy_protocol_version),
      "capabilities": Json::object(caps),
      "clientInfo": Json::object({
        "name": Json::string(self.client_name),
        "version": Json::string(self.client_version),
      }),
    }),
  }).stringify()
  // Send + receive (legacy synchronous send/receive on the transport).
  let t = self.transport
  t.send(request) catch {
    e => return Err(@types.InternalError("Send failed: " + e.to_string()))
  }
  let response = t.receive() catch {
    e => return Err(@types.InternalError("Receive failed: " + e.to_string()))
  }
  let response_str = match response {
    Some(s) => s
    None =>
      return Err(@types.InternalError("Connection closed during initialize"))
  }
  let info = match parse_initialize_response(response_str) {
    Ok(i) => i
    Err(e) => return Err(e)
  }
  self.server_name = info.name
  self.server_version = info.version
  // Send notifications/initialized to complete the handshake.
  let notification = @types.Notification::{
    method_name: "notifications/initialized",
    params: None,
  }
  let _ = t.send_notification(notification) catch { _ => () }
  Ok(info)
}

///|
/// Send a raw JSON-RPC request string and wait for the response (legacy
/// synchronous send/receive). Callers build the request body themselves.
pub async fn LegacyClient::send_raw(
  self : LegacyClient,
  request : String,
) -> Result[String, @types.MCPError] {
  let t = self.transport
  t.send(request) catch {
    e => return Err(@types.InternalError("Send failed: " + e.to_string()))
  }
  let response = t.receive() catch {
    e => return Err(@types.InternalError("Receive failed: " + e.to_string()))
  }
  match response {
    Some(s) => Ok(s)
    None => Err(@types.InternalError("Connection closed"))
  }
}

///|
/// Build a JSON-RPC `ping` request string for the given id.
fn build_ping_request(id : Int) -> String {
  Json::object({
    "jsonrpc": Json::string("2.0"),
    "id": Json::number(id.to_double()),
    "method": Json::string("ping"),
  }).stringify()
}

///|
/// Send a legacy `ping` request and wait for a response.
/// Returns `Ok(())` if the response contains no JSON-RPC error.
pub async fn LegacyClient::ping(
  self : LegacyClient,
) -> Result[Unit, @types.MCPError] {
  let id = self.next_request_id()
  let request = build_ping_request(id)
  let response = self.send_raw(request)
  match response {
    Ok(body) => {
      let json = @json.parse(body) catch {
        _ => return Err(@types.ParseError("Invalid ping response"))
      }
      match json {
        Object(obj) =>
          match obj.get("error") {
            Some(Object(err)) => {
              let message = match err.get("message") {
                Some(String(s)) => s
                _ => "ping failed"
              }
              Err(@types.InternalError(message))
            }
            _ => Ok(())
          }
        _ => Err(@types.ParseError("Ping response is not an object"))
      }
    }
    Err(e) => Err(e)
  }
}

///|
/// Create a legacy client connected over the 2025-11-25 Streamable HTTP session
/// transport and complete the `initialize` handshake.
pub async fn LegacyClient::connect_http(
  url~ : String,
  name~ : String,
  version~ : String,
  auth_token? : String = "",
) -> Result[LegacyClient, @types.MCPError] {
  let http_transport = LegacyHttpSessionTransport::LegacyHttpSessionTransport(
    url~,
    auth_token~,
  )
  let client : LegacyClient = {
    client_name: name,
    client_version: version,
    transport: LegacyTransport::HttpSession(http_transport),
    next_id: 0,
    server_name: "unknown",
    server_version: "unknown",
  }
  match client.initialize() {
    Ok(_) => Ok(client)
    Err(e) => Err(e)
  }
}

///|
/// Close the underlying transport. For the HTTP session transport this also
/// sends a best-effort DELETE to terminate the session.
pub async fn LegacyClient::close(self : LegacyClient) -> Unit {
  self.transport.close()
}