///|
/// A spec path as the route template moonapi reads: each `:name` segment of
/// the spec becomes `{name}`.
pub fn route_path(path : String) -> String {
  path
  .split("/")
  .map(seg => {
    if seg.has_prefix(":") {
      "{" + seg.exact_view(start=1).to_owned() + "}"
    } else {
      seg.to_owned()
    }
  })
  .collect()
  .join("/")
}

///|
/// A group's annotations spelled the way the spec wrote them, e.g. `group: user,
/// prefix: /api/v1, middleware: Log`. Empty for a group that carries nothing.
pub fn group_note(g : @api.Group) -> String {
  let parts : Array[String] = []
  if g.name != "" {
    parts.push("group: " + g.name)
  }
  if g.prefix != "" {
    parts.push("prefix: " + g.prefix)
  }
  if g.jwt != "" {
    parts.push("jwt: " + g.jwt)
  }
  if g.middleware.length() > 0 {
    let mut ms = ""
    for m in g.middleware {
      ms = if ms == "" { m } else { ms + "," + m }
    }
    parts.push("middleware: " + ms)
  }
  if g.max_bytes > 0L {
    parts.push("maxBytes: " + g.max_bytes.to_string())
  }
  if g.timeout != "" {
    parts.push("timeout: " + g.timeout)
  }
  if g.signature {
    parts.push("signature: true")
  }
  for kv in g.extra {
    parts.push(kv.0 + ": " + kv.1)
  }
  let mut out = ""
  for p in parts {
    out = if out == "" { p } else { out + ", " + p }
  }
  out
}

///|
/// Generate compilable moonapi scaffolding from a spec: a MoonBit struct for
/// every `type` block, a `build_app` that wires every route to its handler, plus
/// a stub for each handler. The routing part depends only on `moonbitstack/moonapi`
/// and `moonbitstack/moonasgi`; the emitted schemas are dependency-free.
///
/// Pass `template` to render the spec through the runtime template engine (see
/// `generate_with`) instead of the built-in generator; omit it for the default
/// scaffold.
pub fn generate(
  spec : @api.Spec,
  template? : String,
) -> String raise @template.Refused {
  match template {
    Some(src) => generate_with(spec, src)
    None => generate_builtin(spec)
  }
}

///|
/// A MoonBit struct for every `type` block, in the order the spec declared them,
/// each followed by whatever its tags asked to be checked (see `render_checks`).
pub fn render_structs(
  types : Array[@api.TypeDef],
  derive_json~ : Bool = false,
) -> String {
  let mut out = ""
  for t in types {
    let fields = @api.flat_fields(t, types)
    out = out +
      "///|\n/// `" +
      t.name +
      "` schema (generated from the `.api` `type` block).\n" +
      (if derive_json { "#derive(ToJson, FromJson)\n" } else { "" }) +
      "pub(all) struct " +
      t.name +
      " {\n"
    for f in fields {
      out = out + "  " + f.mbt_name() + " : " + f.type_ + "\n"
    }
    out = out + "}\n\n"
    if derive_json {
      // The derive puts the impl in the type's own package, which promotes its
      // methods onto the type; saying so explicitly is what keeps that a choice
      // rather than a warning.
      out = out +
        "///|\n/// `" +
        t.name +
        "` as JSON, through the derived impls.\npub extend " +
        t.name +
        " with @json.ToJson::{to_json}\n\npub extend " +
        t.name +
        " with @json.FromJson::{from_json}\n\n"
    }
    out = out + render_checks(t.name, fields)
  }
  out
}

///|
pub fn generate_builtin(spec : @api.Spec) -> String {
  let mut out = "// Code generated by moonctl. DO NOT EDIT.\n\n" +
    render_structs(spec.types)
  out = out +
    "///|\n/// Build the " +
    spec.service +
    " application with its routes wired to handlers.\n"
  out = out + "pub fn build_app() -> @moonapi.App {\n"
  out = out + "  let app = @moonapi.App::new()\n"
  // A group's jwt / middleware / limits have no moonapi call to emit them into, so
  // the block they govern is marked with the annotations the spec asked for.
  let mut note = ""
  for r in spec.routes {
    let mark = match r.group {
      Some(g) => group_note(g)
      None => ""
    }
    if mark != note {
      out = out +
        (if mark == "" {
          "  // (ungrouped)\n"
        } else {
          "  // @server " + mark + "\n"
        })
      note = mark
    }
    // moonapi's `ApiHandler` is a *raising* fn-type, and under moonc 0.10.5 a pure
    // named fn no longer coerces to one. Register each handler through an arrow
    // closure — a closure infers the raise effect, so a stub that never raises and a
    // filled-in handler that raises an `HttpException` are both accepted unchanged.
    let handler = "ctx => " + r.handler + "(ctx)"
    let head = "  app." +
      r.verb +
      "(" +
      @text.quote(route_path(r.path)) +
      ", " +
      handler
    out = out +
      (if r.summary != "" {
        head + ", summary=" + @text.quote(r.summary) + ")\n"
      } else {
        head + ")\n"
      })
  }
  out = out + "  app\n}\n\n"
  for r in spec.routes {
    let where_ = match r.group {
      Some(g) if g.name != "" => " of group " + g.name
      _ => ""
    }
    out = out +
      "///|\n/// `" +
      r.verb +
      " " +
      r.path +
      "`" +
      where_ +
      ".\npub fn " +
      r.handler +
      "(_ctx : @moonapi.Context) -> @moonasgi.Response {\n  @moonapi.text(200, " +
      @text.quote("TODO: " + r.handler) +
      ")\n}\n\n"
  }
  out
}