///|
/// Turning parsed results into the text or JSON a caller sees, plus the
/// schema-aware coercion that makes `key=value` arguments pleasant to write.

///|
/// The genuine integer spelling of `text`, or `None`.
///
/// JSON numbers are not bounded by `Int32` and neither are MCP schemas: a
/// millisecond timestamp or a 64-bit id is routinely typed `integer`. Parsing
/// through `Int32` would turn every such value into a JSON *string*, silently
/// breaking the tool call. So a value is accepted when it is plain digits (with
/// an optional sign) and exactly representable as a `Double`; the original
/// text is returned as the `repr` so JSON keeps the full precision instead of
/// printing `1e18` or a rounded mantissa.
fn try_int_literal(raw : String) -> String? {
  let text = raw.trim()
  let chars = text.to_array()
  let start = if chars.length() > 0 && (chars[0] == '-' || chars[0] == '+') {
    1
  } else {
    0
  }
  // Reject `1.5`, `1e9`, `nan`, and an empty (or sign-only) string: those are
  // not integers, and `number` is the type that accepts them.
  if start >= chars.length() {
    return None
  }
  for i in start..= '0' && chars[i] <= '9') {
      return None
    }
  }
  let value : Double = @string.parse_double(text) catch { _ => return None }
  if value.is_nan() || value.is_inf() {
    return None
  }
  // A `Double` holds every integer up to 2^53 exactly; past that the binary
  // value no longer matches what was typed, and `repr` would encode a number
  // the caller never asked for. Comparing `Double`s cannot detect this
  // (2^53+1 and 2^53 compare equal), so the text goes through `Int64` first,
  // whose accepted range is checked directly.
  let exact : Int64 = @string.parse_int64(text) catch { _ => return None }
  if exact > 9007199254740992L || exact < -9007199254740992L {
    return None
  }
  let normalized : String = if text.has_prefix("+") {
    text[1:].to_owned()
  } else {
    text.to_owned()
  }
  Some(normalized)
}

///|
/// Parse a finite double, or `None` when the text is not one. Non-JSON numeric
/// tokens such as `Infinity` and `NaN` stay strings: serialising them as JSON
/// numbers would emit invalid JSON (or, in JavaScript, `null`).
fn try_double(raw : String) -> Double? {
  let value : Double = @string.parse_double(raw.trim()) catch {
    _ => return None
  }
  if value.is_nan() || value.is_inf() {
    None
  } else {
    Some(value)
  }
}

///|
/// Coerce a `key=value` argument using the tool's schema. This is where mcpbox
/// improves on a plain string passthrough: `count=5` becomes a JSON number and
/// `tags=a,b` a JSON array when the schema says so, so callers do not have to
/// hand-write JSON for ordinary values. Anything that does not parse falls back
/// to the string, and `key:=` is always available for the exact form.
pub fn coerce_value(raw : String, schema : Json?) -> Json {
  match schema {
    None => Json::string(raw)
    Some(definition) =>
      match schema_type(definition) {
        "integer" =>
          match try_int_literal(raw) {
            Some(value) => {
              let number : Double = @string.parse_double(value) catch {
                _ => 0.0
              }
              Json::number(number, repr=value)
            }
            None => Json::string(raw)
          }
        "number" =>
          match try_double(raw) {
            Some(value) => Json::number(value)
            None => Json::string(raw)
          }
        "boolean" =>
          match raw.trim() {
            "true" => Json::boolean(true)
            "false" => Json::boolean(false)
            _ => Json::string(raw)
          }
        "array" => {
          let item_schema = match definition {
            Object(obj) => obj.get("items")
            _ => None
          }
          let items : Array[Json] = []
          for item in raw.split(",") {
            items.push(coerce_value(item.trim().to_owned(), item_schema))
          }
          Json::array(items)
        }
        "object" => @json.parse(raw) catch { _ => Json::string(raw) }
        _ => Json::string(raw)
      }
  }
}

///|
/// The separator that owns a `key=value` token, and whether it is the JSON form.
///
/// The left-most separator wins, so `url:http://h/?a=1` keeps the whole URL as
/// the value instead of splitting on the `=` inside it. `:=` is an alias for
/// `=` and counts as a single separator, so the `:` in it does not win by
/// accident.
///
/// Returns `(key, value, is_json)`.
fn split_argument(pair : String) -> (String, String, Bool) raise McpError {
  let chars = pair.to_array()
  let eq = pair.find("=")
  let colon = pair.find(":")
  // Walk to the first separator, preferring the earlier of the two.
  let separator = match (eq, colon) {
    (Some(a), Some(b)) => Some(if a <= b { a } else { b })
    (Some(a), None) => Some(a)
    (None, Some(b)) => Some(b)
    (None, None) => None
  }
  guard separator is Some(index) else {
    raise fail("'" + pair + "' is not a key=value argument")
  }
  // `key:=json`: a colon immediately followed by `=`. The value starts after
  // the `=`, and the key is what came before the `:`.
  let colon_assign = chars[index] == ':' &&
    index + 1 < chars.length() &&
    chars[index + 1] == '='
  let key_end = index
  let value_start = if colon_assign { index + 2 } else { index + 1 }
  let is_json = colon_assign
  let key = pair.exact_view(end=key_end).to_owned()
  if key.length() == 0 {
    raise fail("'" + pair + "' has an empty key")
  }
  (key, pair.exact_view(start=value_start).to_owned(), is_json)
}

///|
/// Build the `arguments` object for a tool call.
pub fn build_arguments(
  pairs : Array[String],
  schema : Json,
) -> Json raise McpError {
  let properties = schema_properties(schema)
  let fields : Map[String, Json] = Map([])
  for pair in pairs {
    let (key, raw, is_json) = split_argument(pair)
    fields[key] = if is_json {
      parse_jsonc(raw)
    } else {
      coerce_value(raw, properties.get(key))
    }
  }
  Json::object(fields)
}

///|
/// The JSON view of a server's `initialize` result.
pub fn server_info_to_json(info : ServerInfo) -> Json {
  let fields : Map[String, Json] = {
    "name": Json::string(info.name),
    "version": Json::string(info.version),
    "protocolVersion": Json::string(info.protocol_version),
  }
  if info.instructions.length() > 0 {
    fields["instructions"] = Json::string(info.instructions)
  }
  Json::object(fields)
}

///|
/// The names-only discovery view. This path never connects to a server.
fn render_server_names(servers : Array[Server]) -> String {
  let names : Array[String] = []
  for server in servers {
    names.push(server.name)
  }
  names.join("\n")
}

///|
/// The JSON view of configured servers without contacting any of them.
fn render_servers_json(servers : Array[Server]) -> String {
  let items : Array[Json] = []
  for server in servers {
    items.push(server_to_json(server))
  }
  Json::object({ "servers": Json::array(items) }).stringify(indent=2)
}

///|
/// Render the `mcpbox list` result as JSON.
fn render_reports_json(reports : Array[ServerReport]) -> String {
  let servers : Array[Json] = []
  for report in reports {
    let fields : Map[String, Json] = match server_to_json(report.server) {
      Object(obj) => obj
      _ => Map([])
    }
    match report.info {
      Some(info) => fields["serverInfo"] = server_info_to_json(info)
      None => ()
    }
    match report.error {
      Some(message) => fields["error"] = Json::string(message)
      None => ()
    }
    let tools : Array[Json] = []
    for tool in report.tools {
      tools.push(tool_to_json(tool))
    }
    fields["tools"] = Json::array(tools)
    servers.push(Json::object(fields))
  }
  Json::object({ "servers": Json::array(servers) }).stringify(indent=2)
}

///|
/// Render the `mcpbox list` result for a human.
fn render_reports_text(reports : Array[ServerReport], opts : Options) -> String {
  let out = StringBuilder()
  let mut first = true
  for report in reports {
    if !first {
      out.write_string("\n")
    }
    first = false
    if opts.brief {
      out.write_string("\{report.server.name}  ")
      match report.error {
        Some(message) => out.write_string("error: " + message)
        None => out.write_string("(\{report.tools.length()} tools)")
      }
      continue
    }
    out.write_string(
      "\{report.server.name}  \{transport_target(report.server)}",
    )
    match report.info {
      Some(info) =>
        if info.name.length() > 0 || info.version.length() > 0 {
          out.write_string("  [\{info.name} \{info.version}]")
        }
      None => ()
    }
    out.write_string("\n")
    if !opts.no_docs {
      match report.info {
        Some(info) =>
          for line in render_instructions(info.instructions, "  ") {
            out.write_string(line + "\n")
          }
        None => ()
      }
    }
    match report.error {
      Some(message) => out.write_string("  error: " + message + "\n")
      None =>
        if report.tools.length() == 0 {
          out.write_string("  (no tools)\n")
        } else {
          let mut first_tool = true
          for tool in report.tools {
            if !first_tool && !opts.no_docs {
              out.write_string("\n")
            }
            first_tool = false
            if !opts.no_docs {
              for line in render_tool_doc(tool, "  ") {
                out.write_string(line + "\n")
              }
            }
            out.write_string("  " + render_tool_signature(tool) + "\n")
          }
        }
    }
  }
  out.to_string()
}

///|
/// Render a tool call result according to the output flags.
fn render_call_output(result : CallResult, opts : Options) -> String {
  if opts.json {
    return call_result_to_json(result).stringify()
  }
  if opts.raw {
    let out = StringBuilder()
    for block in result.content {
      match block {
        Text(text) => out.write_string(text)
        _ => ()
      }
    }
    return out.to_string()
  }
  render_call_result(result)
}