// `gen mcp `: the spec's routes as an MCP server.
//
// A tool is an annotated function — `#mcp.tool(description=…)` — and moonmcp derives
// the JSON input schema from its parameters, so the generator writes the functions
// and the registration a first build is going to rebuild anyway. The request type a
// route declares becomes the tool's arguments: a client supplies the same values an
// HTTP caller would put in the body, the query string or the path.
//
// The function bodies are the route's `TODO`: moonctl does not know what the tool
// should answer, and a stub that made something up would be worse than one that says
// so.

///|
/// The JSON schema type of a MoonBit type — `string`, `integer`, `number`, `boolean`
/// or an array of one — or `None` when the type has no schema of its own and is
/// carried as JSON text instead.
fn mcp_json_type(ty : String) -> String? {
  if @text.ends_with(ty, "?") {
    return mcp_json_type(ty[0:ty.length() - 1].to_owned())
  }
  if @text.starts_with(ty, "Array[") {
    let inner = ty[6:ty.length() - 1].to_owned()
    return match mcp_json_type(inner) {
      Some(t) => Some("array:" + t)
      None => None
    }
  }
  match ty {
    "String" => Some("string")
    "Bool" => Some("boolean")
    "Int32" | "Int64" | "UInt32" | "UInt64" | "Byte" => Some("integer")
    "Double" | "Float" => Some("number")
    "Bytes" => Some("string")
    _ => None
  }
}

///|
/// The `{ "type": … }` a property of the input schema carries.
fn mcp_schema_of(ty : String) -> String {
  match mcp_json_type(ty) {
    Some(t) =>
      if @text.starts_with(t, "array:") {
        "{\"type\": \"array\", \"items\": {\"type\": \"" +
        t[6:].to_owned() +
        "\"}}"
      } else {
        "{ \"type\": \"" + t + "\" }"
      }
    None => "{ \"type\": \"string\", \"description\": \"JSON text\" }"
  }
}

///|
/// The parameters a tool takes, from the request type `req` (`req = ""` for a route
/// that declares none): `slot : ty` for a value the caller must send, `slot? : ty`
/// for one it may leave out.
fn mcp_params(spec : @api.Spec, req : String) -> Array[(String, String, Bool)] {
  let out : Array[(String, String, Bool)] = []
  if req == "" {
    return out
  }
  for t in spec.types {
    if t.name != req {
      continue
    }
    for f in @api.flat_fields(t, spec.types) {
      let ty = f.type_
      let optional = @text.ends_with(ty, "?")
      let base = if optional { ty[0:ty.length() - 1].to_owned() } else { ty }
      // the declared type travels through; a value with no schema of its own is a
      // `String` in the signature and JSON text in the schema
      out.push((f.mbt_name(), base, optional))
    }
  }
  out
}

///|
/// `s` as a MoonBit string literal, quotes and backslashes escaped.
fn str_lit(s : String) -> String {
  let out = StringBuilder()
  out.write_char('"')
  for i = 0; i < s.length(); i = i + 1 {
    let c = s[i]
    if c == '"' {
      out.write_string("\\\"")
    } else if c == '\\' {
      out.write_string("\\\\")
    } else {
      out.write_string(s[i:i + 1].to_owned())
    }
  }
  out.write_char('"')
  out.to_string()
}

///|
/// The tool's name: its handler, spelled the way a MoonBit function is.
fn mcp_tool_name(r : @api.Route) -> String {
  @text.to_snake(r.handler)
}

///|
/// The description a tool carries: the route's `@doc`, or the verb and path when the
/// spec gave none.
fn mcp_description(r : @api.Route) -> String {
  if r.summary != "" {
    r.summary
  } else {
    r.verb + " " + r.path
  }
}

///|
/// One `#mcp.tool` function: the route's request fields as parameters, and a body
/// that says the route is not wired to anything yet.
fn mcp_tool_fn(
  r : @api.Route,
  name : String,
  params : Array[(String, String, Bool)],
) -> String {
  let mut signature = ""
  if params.length() == 0 {
    signature = "pub fn " + name + "() -> String {\n"
  } else {
    signature = "pub fn " + name + "(\n"
    for p in params {
      signature = signature +
        "  " +
        p.0 +
        (if p.2 { "?" } else { "" }) +
        " : " +
        (if mcp_json_type(p.1) is None { "String" } else { p.1 }) +
        ",\n"
    }
    signature = signature + ") -> String {\n"
  }
  // The parameters keep their names — a tool's input schema is derived from them —
  // and the stub body acknowledges them, so a freshly generated server is free of
  // unused-parameter warnings.
  let slots : Array[String] = []
  for p in params {
    slots.push(p.0)
  }
  let body = if slots.length() == 0 {
    "  " + str_lit("TODO: " + r.handler) + "\n"
  } else {
    "  ignore((" +
    slots.join(", ") +
    "))\n  " +
    str_lit("TODO: " + r.handler) +
    "\n"
  }
  "///|\n/// `" +
  r.verb +
  " " +
  r.path +
  "` — the tool `" +
  name +
  "`.\n#mcp.tool(description=" +
  str_lit(mcp_description(r)) +
  ")\n" +
  signature +
  body +
  "}\n\n"
}

///|
/// The `mcp.tool(…)` call that registers one tool: the arguments moonmcp decodes,
/// the schema they describe, and the description.
fn mcp_tool_call(
  r : @api.Route,
  name : String,
  params : Array[(String, String, Bool)],
) -> String {
  let mut args = ""
  let mut props = ""
  let required : Array[String] = []
  for p in params {
    args = args +
      (if p.2 {
        "      " +
        p.0 +
        "?=@moonmcp.decode_option(args, " +
        str_lit(p.0) +
        "),\n"
      } else {
        "      @moonmcp.decode(args, " + str_lit(p.0) + "),\n"
      })
    props = props +
      "        " +
      str_lit(p.0) +
      ": " +
      mcp_schema_of(p.1) +
      ",\n"
    if p.2 == false {
      required.push(str_lit(p.0))
    }
  }
  let call = if params.length() == 0 {
    "_args => ToJson::to_json(" + name + "())"
  } else {
    "args => ToJson::to_json(" + name + "(\n" + args + "    ))"
  }
  let schema = "{ \"type\": \"object\", \"properties\": {\n" +
    props +
    "      }" +
    (if required.length() == 0 {
      ""
    } else {
      ",\n      \"required\": [" + required.join(", ") + "]"
    }) +
    "\n    }"
  "  mcp.tool(\n    " +
  str_lit(name) +
  ",\n    " +
  call +
  ",\n    input_schema=" +
  schema +
  ",\n    description=" +
  str_lit(mcp_description(r)) +
  ",\n  )\n"
}

///|
/// The routes a tool is offered for, each with the name it is registered under: a
/// route whose request type declares no field would be a tool a client can call with
/// nothing to send, and moonmcp's own registration for it carries an unused-argument
/// warning, so those routes stay HTTP-only and are reported instead.
fn mcp_offered(
  spec : @api.Spec,
) -> Array[(@api.Route, String, Array[(String, String, Bool)])] {
  let out : Array[(@api.Route, String, Array[(String, String, Bool)])] = []
  let taken : Array[String] = []
  for r in spec.routes {
    let params = mcp_params(spec, r.req)
    if params.length() == 0 {
      continue
    }
    let base = mcp_tool_name(r)
    let mut name = base
    let mut n = 2
    while taken.contains(name) {
      name = base + "_" + n.to_string()
      n = n + 1
    }
    taken.push(name)
    out.push((r, name, params))
  }
  out
}

///|
/// The routes a generated server does not offer, each as ` `: they
/// declare no request type, so there is nothing for a tool to take.
pub fn mcp_skipped(spec : @api.Spec) -> Array[String] {
  let out : Array[String] = []
  for r in spec.routes {
    if mcp_params(spec, r.req).length() == 0 {
      out.push(r.verb + " " + r.path)
    }
  }
  out
}

///|
/// `internal/tools/tools.mbt` for `spec`: one annotated function per offered route.
pub fn mcp_tools_for(spec : @api.Spec) -> String {
  let offered = mcp_offered(spec)
  let mut out = "///|\n/// The tools of the " +
    spec.service +
    " server: one per route, its parameters the route's request type.\n/// Each annotated function is offered by the server; its parameters become the\n/// tool's input schema. Edit freely: `tools.g.mbt` is regenerated on build.\n"
  for entry in offered {
    out = out + "\n" + mcp_tool_fn(entry.0, entry.1, entry.2)
  }
  out
}

///|
/// `internal/tools/tools.g.mbt`: what moonmcp regenerates from those functions, so a
/// fresh project compiles before its first build.
pub fn mcp_registration_for(spec : @api.Spec) -> String {
  let offered = mcp_offered(spec)
  let mut out = "// Generated by moonmcp from tools.mbt. Edit those files, not this one.\n\n///|\npub fn register(mcp : @moonmcp.Server) -> Unit {\n"
  for entry in offered {
    out = out + mcp_tool_call(entry.0, entry.1, entry.2)
  }
  out + "}\n"
}

///|
/// The test a spec-derived server is seeded with: every offered route registered as a
/// tool, counted through `tools/list`.
pub fn mcp_checks_for(spec : @api.Spec) -> String {
  let n = mcp_offered(spec).length().to_string()
  let src =
    #|///|
    #|async test "every route with a request type is a tool" {
    #|  let mcp = @moonmcp.Server::new("test")
    #|  @tools.register(mcp)
    #|  let call : Json = {
    #|    "jsonrpc": "2.0",
    #|    "id": 1,
    #|    "method": "tools/list",
    #|    "params": {
    #|      "_meta": {
    #|        "io.modelcontextprotocol/protocolVersion": "2026-07-28",
    #|        "io.modelcontextprotocol/clientCapabilities": {},
    #|      },
    #|    },
    #|  }
    #|  guard mcp.handle(call)
    #|    is Some({ "result": { "tools": Array(list), .. }, .. }) else {
    #|    fail("no tools in the reply")
    #|  }
    #|  assert_eq(list.length(), %n)
    #|}
  src.replace_all(old="%n", new=n)
}

///|
/// The MCP server tree for the service `spec` describes: the same module, packages
/// and build rule as `generate_mcp_tree`, with the spec's routes as its tools.
pub fn generate_mcp_from_spec(
  spec : @api.Spec,
  style? : @style.Style,
  dir~ : String = "",
) -> Array[TreeFile] {
  generate_mcp_tree(
    spec.service,
    style?,
    dir~,
    tools=mcp_tools_for(spec),
    registration=mcp_registration_for(spec),
    checks=mcp_checks_for(spec),
  )
}