// `mctl api doc`: the reference a team reads, as Markdown (goctl's `api doc`).
//
// The OpenAPI document in `doc.mbt` is for a machine — a generator, a gateway, a
// test suite. This one is for the person who has to call the service: a table of
// routes, then one table per type saying where each field is bound and what the
// spec constrains it to. Markdown rather than HTML so it can be committed beside
// the spec and diffed when the spec changes.

///|
/// One table cell: the text with the `|` that would break a row escaped, and the
/// newlines a multi-line value carries folded into spaces.
fn cell(s : String) -> String {
  let mut out = ""
  let mut start = 0
  for i = 0; i < s.length(); i = i + 1 {
    let c = s[i]
    if c == '|' || c == '\n' || c == '\r' {
      out = out + s[start:i].to_owned() + (if c == '|' { "\\|" } else { " " })
      start = i + 1
    }
  }
  @text.trim(out + s[start:].to_owned())
}

///|
/// Where a field is bound, as a word a reader recognises.
fn bind_word(f : @api.Field) -> String {
  match f.bind() {
    @api.Body => "body"
    @api.Query => "query"
    @api.Path => "path"
    @api.Header => "header"
  }
}

///|
/// The constraints a tag carries — `default=`, `options=`, `range=`, `optional` —
/// as one line, or an empty string when the tag carries none.
fn constraints(f : @api.Field) -> String {
  let parts : Array[String] = []
  // `options=free|pro` is one constraint with two permitted values, so it reads as
  // one phrase — "one of `free`, `pro`" — rather than the phrase twice.
  let allowed = f.options()
  if allowed.length() > 0 {
    let mut listed = ""
    for i = 0; i < allowed.length(); i = i + 1 {
      if i > 0 {
        listed = listed + ", "
      }
      listed = listed + "`" + allowed[i] + "`"
    }
    parts.push("one of " + listed)
  }
  match f.default_() {
    Some(v) => parts.push("default `" + v + "`")
    None => ()
  }
  match f.range() {
    Some(r) => {
      let lo = if r.lo == "" { "" } else { r.lo }
      let hi = if r.hi == "" { "" } else { r.hi }
      parts.push(
        "range " +
        (if r.lo_inc { "[" } else { "(" }) +
        lo +
        ".." +
        hi +
        (if r.hi_inc { "]" } else { ")" }),
      )
    }
    None => ()
  }
  if f.optional() {
    parts.push("optional")
  }
  parts.join(", ")
}

///|
/// The routes of a spec as one table, in spec order.
fn routes_table(spec : @api.Spec) -> Array[String] {
  let out : Array[String] = []
  out.push("| Method | Path | Handler | Request | Response | Description |")
  out.push("| --- | --- | --- | --- | --- | --- |")
  for r in spec.routes {
    out.push(
      "| " +
      cell(r.verb.to_upper()) +
      " | `" +
      cell(r.path) +
      "` | `" +
      cell(r.handler) +
      "` | " +
      (if r.req == "" { "—" } else { "`" + cell(r.req) + "`" }) +
      " | " +
      (if r.resp == "" { "—" } else { "`" + cell(r.resp) + "`" }) +
      " | " +
      cell(if r.summary == "" { "—" } else { r.summary }) +
      " |",
    )
  }
  out
}

///|
/// One type block as a table: the wire name, the MoonBit type, where the value is
/// bound from, whether the caller has to send it, and what the tag constrains.
fn type_table(t : @api.TypeDef, types : Array[@api.TypeDef]) -> Array[String] {
  let out : Array[String] = []
  out.push("### " + t.name)
  out.push("")
  let fields = @api.flat_fields(t, types)
  if fields.length() == 0 {
    out.push("_The block declares no field._")
    out.push("")
    return out
  }
  out.push("| Field | Type | In | Required | Constraints |")
  out.push("| --- | --- | --- | --- | --- |")
  for f in fields {
    out.push(
      "| `" +
      cell(f.json_name()) +
      "` | `" +
      cell(f.type_) +
      "` | " +
      bind_word(f) +
      " | " +
      (if f.optional() { "no" } else { "yes" }) +
      " | " +
      cell(constraints(f)) +
      " |",
    )
  }
  out.push("")
  out
}

///|
/// The whole reference: the service, a route table, then a table per type. `title`
/// replaces the service name in the heading for a reader who wants the document to
/// say what the service is for.
pub fn generate_markdown(spec : @api.Spec, title? : String) -> String {
  let out : Array[String] = []
  let heading = match title {
    Some(t) => t
    None => if spec.service == "" { "API" } else { spec.service }
  }
  out.push("# " + heading)
  out.push("")
  let summary = "A MoonAPI service with " +
    spec.routes.length().to_string() +
    " route(s) and " +
    spec.types.length().to_string() +
    " type(s)."
  out.push(summary)
  out.push("")
  if spec.info.length() > 0 {
    out.push("| Key | Value |")
    out.push("| --- | --- |")
    for kv in spec.info {
      out.push("| " + cell(kv.0) + " | " + cell(kv.1) + " |")
    }
    out.push("")
  }
  out.push("## Routes")
  out.push("")
  if spec.routes.length() == 0 {
    out.push("_The service declares no route._")
  } else {
    for line in routes_table(spec) {
      out.push(line)
    }
    if spec.groups.length() > 0 {
      out.push("")
      out.push("### Groups")
      out.push("")
      out.push("| Group | Prefix | JWT | Middleware | Max body | Timeout |")
      out.push("| --- | --- | --- | --- | --- | --- |")
      for g in spec.groups {
        out.push(
          "| `" +
          cell(g.name) +
          "` | `" +
          cell(g.prefix) +
          "` | " +
          (if g.jwt == "" { "—" } else { "`" + cell(g.jwt) + "`" }) +
          " | " +
          (if g.middleware.length() == 0 {
            "—"
          } else {
            g.middleware.map(fn(m) { "`" + m + "`" }).join(", ")
          }) +
          " | " +
          (if g.max_bytes == 0L { "—" } else { g.max_bytes.to_string() }) +
          " | " +
          (if g.timeout == "" { "—" } else { "`" + cell(g.timeout) + "`" }) +
          " |",
        )
      }
    }
  }
  out.push("")
  out.push("## Types")
  out.push("")
  if spec.types.length() == 0 {
    out.push("_The spec declares no type._")
    out.push("")
  } else {
    for t in spec.types {
      for line in type_table(t, spec.types) {
        out.push(line)
      }
    }
  }
  out.push("---")
  out.push("")
  out.push("Generated by `mctl api doc`. Edit the `.api` spec, not this file.")
  @text.join_lines(out)
}

///|
/// The file name a Markdown reference lands in: `greet` → `greet_api.md`.
pub fn markdown_file_name(service : String) -> String {
  (if service == "" { "api" } else { @text.to_snake(service) }) + "_api.md"
}