// The layered project tree, and what a second run of it is allowed to touch.
//
// goctl does not write one file: it writes `etc/.yaml`, `internal/config`,
// `internal/svc`, `internal/types`, `internal/handler` (routes plus one file per
// handler, under the group's own directory when the route is grouped),
// `internal/logic`, `internal/middleware` and the service's entry point. This is the
// same tree in MoonBit, one package per directory.
//
// The second run is the point of it. goctl regenerates only the two machine-owned
// files — the routes and the types — and skips every file that is already on disk,
// because those are where the author's code lives. Each file here therefore says
// which it is, and `tree_plan` is what turns a tree plus "does this exist?" into the
// files to actually write.

///|
/// Whether a regeneration may replace a file that is already on disk. `Always` is
/// for what moonctl owns — the routes and the types, which must follow the spec —
/// and `Once` for what it only seeds: handlers, logic, configuration, manifests.
#derive(Eq)
pub(all) enum Regen {
  Always
  Once
}

///|
pub extend Regen with Eq::{equal, not_equal}

///|
/// One file of a generated project tree: where it goes, what is in it, and whether
/// regenerating the tree is allowed to overwrite it.
pub(all) struct TreeFile {
  path : String
  content : String
  regen : Regen
}

///|
/// `rest` under `dir`, or `rest` alone when `dir` is empty.
fn at(dir : String, rest : String) -> String {
  if dir == "" {
    rest
  } else {
    dir + "/" + rest
  }
}

///|
/// `xs` with later repeats dropped, order kept.
fn uniq(xs : Array[String]) -> Array[String] {
  let out : Array[String] = []
  for x in xs {
    let mut seen = false
    for y in out {
      if y == x {
        seen = true
      }
    }
    if seen == false {
      out.push(x)
    }
  }
  out
}

///|
/// The directory a route's handler and logic live in — its group, spelled in the
/// style — or "" for a route declared outside any `@server` block.
fn route_dir(r : @api.Route, style : @style.Style) -> String {
  match r.group {
    Some(g) if g.name != "" => style.format(g.name)
    _ => ""
  }
}

///|
/// Generate the layered project tree goctl writes, in MoonBit: the service entry
/// point and its `moon.mod`, `etc/.yaml`, `internal/config`,
/// `internal/svc`, `internal/types`, `internal/handler` (the generated routes plus a
/// stub per handler, under `internal/handler//` for a grouped route),
/// `internal/logic`, `internal/middleware` for every middleware an `@server` block
/// named, and a `moon.pkg` for each of those packages.
///
/// `style` (default `gozero`) names the files and the handler, logic and middleware
/// entry points inside them. Names the spec itself chose — the `type` blocks and
/// their fields — are left as written, since a MoonBit type name has to keep its
/// capital. `dir` puts the whole tree under a directory.
///
/// Each file says whether a regeneration may overwrite it; hand the result to
/// `tree_plan` to apply that.
pub fn generate_tree(
  spec : @api.Spec,
  style? : @style.Style,
  dir~ : String = "",
) -> Array[TreeFile] {
  let st = match style {
    Some(s) => s
    None => @style.Style::gozero()
  }
  let mod_ = spec.service
  let config_pkg = mod_ + "/internal/config"
  let svc_pkg = mod_ + "/internal/svc"
  let handler_pkg = mod_ + "/internal/handler"
  let logic_pkg = mod_ + "/internal/logic"
  let types_pkg = mod_ + "/internal/types"
  let api = ("moonbitstack/moonapi", "")
  let asgi = ("moonbitstack/moonasgi", "")

  let out = base_tree(
    mod_,
    st,
    dir,
    deps=[
      "moonbitstack/moonapi", "moonbitstack/moonasgi", "moonbitstack/mooncat",
    ],
    imports=[api, ("moonbitstack/mooncat", ""), (handler_pkg, "")],
    main=asgi_main(
      mod_, "HTTP", "@handler.register_handlers(@moonapi.App::new(), svc).to_asgi()",
    ),
  )

  // internal/types — the spec's schemas and the binders the handlers call,
  // rewritten on every run.
  let binders = @gen.render_binders(spec)
  out.push({
    path: at(dir, "internal/types/moon.pkg"),
    content: pkg_file(types_imports(spec, binders)),
    regen: Once,
  })
  out.push({
    path: at(dir, "internal/types/" + st.format("types") + ".mbt"),
    content: "// Code generated by moonctl. DO NOT EDIT.\n\n" +
    @gen.render_structs(spec.types, derive_json=true) +
    binders,
    regen: Always,
  })

  // internal/handler — the routes, rewritten on every run, and a stub per handler.
  let dirs : Array[String] = []
  for r in spec.routes {
    dirs.push(route_dir(r, st))
  }
  let group_dirs : Array[String] = []
  let mut ungrouped = false
  for d in uniq(dirs) {
    if d == "" {
      ungrouped = true
    } else {
      group_dirs.push(d)
    }
  }
  let routes_imports : Array[(String, String)] = [api, (svc_pkg, "")]
  if ungrouped {
    routes_imports.push(asgi)
    routes_imports.push((logic_pkg, "logic"))
    if binds_in(spec, st, "") {
      routes_imports.push((types_pkg, ""))
    }
  }
  for d in group_dirs {
    routes_imports.push((handler_pkg + "/" + d, d))
  }
  // The test's imports do not depend on the spec, so a regeneration never leaves
  // the seeded test without them.
  let tests = pkg_file([
    ("moonbitstack/moonasgi/testing", ""),
    ("moonbitlang/async", ""),
    (config_pkg, ""),
  ])
  out.push({
    path: at(dir, "internal/handler/moon.pkg"),
    content: pkg_file(routes_imports) +
    "\n" +
    tests.replace(old="}\n", new="} for \"test\"\n"),
    regen: Always,
  })
  let ping =
    #|
    #|  let reply = client.get("/ping")
    #|  assert_eq(reply.status, 200)
    #|  assert_eq(reply.text(), "pong")
  let test_src =
    #|///|
    #|async test "the app answers its routes and nothing else" {
    #|  let svc = @svc.ServiceContext::new(@config.Config::new())
    #|  let app = @handler.register_handlers(@moonapi.App::new(), svc)
    #|  let client = @testing.Client::new(app.to_asgi())
    #|  assert_eq(client.get("/no/such/route").status, 404)%ping
    #|}
  let pings = spec.routes.any(r => r.verb == "get" && r.path == "/ping")
  out.push({
    path: at(dir, "internal/handler/" + st.format("routes") + "_test.mbt"),
    content: test_src.replace(old="%ping", new=if pings { ping } else { "" }) +
    "\n",
    regen: Once,
  })
  out.push({
    path: at(dir, "internal/handler/" + st.format("routes") + ".mbt"),
    content: render_routes(spec, st),
    regen: Always,
  })
  for d in group_dirs {
    out.push({
      path: at(dir, "internal/handler/" + d + "/moon.pkg"),
      content: pkg_file(
        group_handler_imports(
          api,
          asgi,
          svc_pkg,
          logic_pkg,
          types_pkg,
          d,
          binds_in(spec, st, d),
        ),
      ),
      regen: Once,
    })
  }
  for r in spec.routes {
    let d = route_dir(r, st)
    let name = st.format(r.handler + "_handler")
    out.push({
      path: at(dir, at("internal/handler", at(d, name + ".mbt"))),
      content: api_handler_src(spec, st, r, name),
      regen: Once,
    })
  }

  // internal/logic — one package per group, mirroring the handlers.
  let logic_dirs = if ungrouped { [""] } else { [] }
  for d in group_dirs {
    logic_dirs.push(d)
  }
  for d in logic_dirs {
    out.push({
      path: at(dir, at("internal/logic", at(d, "moon.pkg"))),
      content: pkg_file(
        if binds_in(spec, st, d) {
          [api, asgi, (svc_pkg, ""), (types_pkg, "")]
        } else {
          [api, asgi, (svc_pkg, "")]
        },
      ),
      regen: Once,
    })
  }
  for r in spec.routes {
    let d = route_dir(r, st)
    let name = st.format(r.handler + "_logic")
    out.push({
      path: at(dir, at("internal/logic", at(d, name + ".mbt"))),
      content: api_logic_src(spec, r, name),
      regen: Once,
    })
  }

  // internal/middleware — one per name any @server block asked for.
  let names : Array[String] = []
  for g in spec.groups {
    for m in g.middleware {
      names.push(m)
    }
  }
  let middleware = uniq(names)
  if middleware.length() > 0 {
    out.push({
      path: at(dir, "internal/middleware/moon.pkg"),
      content: pkg_file([api, asgi]),
      regen: Once,
    })
  }
  for m in middleware {
    let name = st.format(m + "_middleware")
    out.push({
      path: at(dir, "internal/middleware/" + name + ".mbt"),
      content: "///|\n/// The " +
      m +
      " middleware of the " +
      spec.service +
      " service: wrap the handler it is\n/// given and return the wrapper.\npub fn " +
      name +
      "(\n  next : (@moonapi.Context) -> @moonasgi.Response,\n) -> (@moonapi.Context) -> @moonasgi.Response {\n  next\n}\n",
      regen: Once,
    })
  }
  out
}

///|
/// The imports of the generated `internal/types` package: the framework and JSON
/// when it holds schemas (every one of them carries a derived JSON pair), and the
/// text parsers when it also holds binders.
fn types_imports(spec : @api.Spec, binders : String) -> Array[(String, String)] {
  let out : Array[(String, String)] = []
  // A struct carries derived JSON impls, and nothing else, when no route binds it:
  // the framework and the text parsers are imported only by the binders.
  if spec.types.length() > 0 {
    out.push(("moonbitlang/core/json", ""))
  }
  if binders != "" {
    out.push(("moonbitstack/moonapi", ""))
    if spec.types.length() == 0 {
      out.push(("moonbitlang/core/json", ""))
    }
    if @gen.binders_parse_text(spec) {
      out.push(("moonbitlang/core/string", ""))
    }
  }
  out
}

///|
/// Whether the spec declares a `type` block called `name`.
fn declares(spec : @api.Spec, name : String) -> Bool {
  for t in spec.types {
    if t.name == name {
      return true
    }
  }
  false
}

///|
/// Whether the spec writes a binder for the request type `name`.
fn is_bound(spec : @api.Spec, name : String) -> Bool {
  for entry in @gen.bound_types(spec) {
    if entry.0 == name {
      return true
    }
  }
  false
}

///|
/// The imports of a group's handler package: the framework, the context, the logic
/// package the group calls, and — when this spec has request types — the package
/// holding their binders.
fn group_handler_imports(
  api : (String, String),
  asgi : (String, String),
  svc_pkg : String,
  logic_pkg : String,
  types_pkg : String,
  group : String,
  binds : Bool,
) -> Array[(String, String)] {
  let out : Array[(String, String)] = [
    api,
    asgi,
    (svc_pkg, ""),
    (logic_pkg + "/" + group, "logic"),
  ]
  if binds {
    out.push((types_pkg, ""))
  }
  out
}

///|
/// Whether any route whose handler lives in `group`'s package (`""` for the routes
/// package itself) has a request type the handlers bind.
fn binds_in(spec : @api.Spec, st : @style.Style, group : String) -> Bool {
  for r in spec.routes {
    if route_dir(r, st) == group && r.req != "" && is_bound(spec, r.req) {
      return true
    }
  }
  false
}

///|
/// The handler of one route: bind the request into the type the spec declared,
/// hand it to the logic, and write what the logic answers — a `Result`'s value as
/// JSON when the route declares a response type, its message as `501` when the
/// logic has not been filled in yet. A route that declares no request passes the
/// context through, the way a handler that needs nothing else does.
fn api_handler_src(
  spec : @api.Spec,
  st : @style.Style,
  r : @api.Route,
  name : String,
) -> String {
  let has_req = r.req != "" && is_bound(spec, r.req)
  let has_resp = r.resp != "" && declares(spec, r.resp)
  let call = "@logic." + st.format(r.handler + "_logic")
  let mut body = ""
  if has_req {
    body = body +
      "  let req = match @types.bind_" +
      @text.to_snake(r.req) +
      "(ctx) {\n    Ok(req) => req\n    Err(why) => return @moonapi.text(400, why)\n  }\n"
  }
  let arg = if has_req { "req" } else { "ctx" }
  if has_resp {
    body = body +
      "  match " +
      call +
      "(svc, " +
      arg +
      ") {\n    Ok(reply) => @moonapi.json(200, ToJson::to_json(reply))\n    Err(why) => @moonapi.text(501, why)\n  }\n"
  } else {
    body = body + "  " + call + "(svc, " + arg + ")\n"
  }
  "///|\n/// `" +
  r.verb +
  " " +
  r.path +
  "` — the HTTP edge of the route: read the request, call\n/// the logic, write what comes back.\npub fn " +
  name +
  "(\n  svc : @svc.ServiceContext,\n  ctx : @moonapi.Context,\n) -> @moonasgi.Response {\n" +
  body +
  "}\n"
}

///|
/// The logic of one route, seeded: it takes the request the handler bound (or the
/// context, for a route that declares none) and returns what the route answers.
/// A route with a response type returns `Result`: the value the handler writes as
/// JSON, or the message it answers `501` with while the body is still `TODO`.
fn api_logic_src(spec : @api.Spec, r : @api.Route, name : String) -> String {
  let has_req = r.req != "" && is_bound(spec, r.req)
  let has_resp = r.resp != "" && declares(spec, r.resp)
  let param = if has_req {
    "_req : @types." + r.req
  } else {
    "_ctx : @moonapi.Context"
  }
  let ret = if has_resp {
    "Result[@types." + r.resp + ", String]"
  } else {
    "@moonasgi.Response"
  }
  let body = if has_resp {
    "  Err(" + @text.quote("TODO: " + r.handler) + ")\n"
  } else {
    "  @moonapi.text(200, " +
    @text.quote(if r.handler == "ping" { "pong" } else { "TODO: " + r.handler }) +
    ")\n"
  }
  "///|\n/// `" +
  r.verb +
  " " +
  r.path +
  "`" +
  (if r.summary == "" { "" } else { " — " + r.summary }) +
  ". This is where the service does its work.\npub fn " +
  name +
  "(\n  _svc : @svc.ServiceContext,\n  " +
  param +
  ",\n) -> " +
  ret +
  " {\n" +
  body +
  "}\n"
}

///|
/// The `app.(…)` lines for `routes`, with a comment wherever the `@server`
/// annotations change — the same shape goctl writes above each route group.
fn route_lines(routes : Array[@api.Route], st : @style.Style) -> String {
  let mut out = ""
  let mut note = ""
  for r in routes {
    let mark = match r.group {
      Some(g) => @gen.group_note(g)
      None => ""
    }
    if mark != note {
      out = out +
        (if mark == "" {
          "  // (ungrouped)\n"
        } else {
          "  // @server " + mark + "\n"
        })
      note = mark
    }
    let d = route_dir(r, st)
    let call = (if d == "" { "" } else { "@" + d + "." }) +
      st.format(r.handler + "_handler")
    let head = "  app." +
      r.verb +
      "(" +
      @text.quote(@gen.route_path(r.path)) +
      ", ctx => " +
      call +
      "(svc, ctx)"
    out = out +
      (if r.summary != "" {
        head + ", summary=" + @text.quote(r.summary) + ")\n"
      } else {
        head + ")\n"
      })
  }
  out
}

///|
/// `internal/handler/routes.mbt`: every route registered on the app, each one
/// calling into the handler package its group lives in. Regenerated on every run,
/// which is why nothing of the author's belongs in it.
///
/// A spec with more than one `service` block gets one registration per service,
/// composed by `register_handlers` in the order the blocks were written: a route
/// belongs to the service that declared it, so two services never end up as one
/// undifferentiated list of routes.
fn render_routes(spec : @api.Spec, st : @style.Style) -> String {
  let svc = if spec.routes.length() == 0 { "_svc" } else { "svc" }
  let head = "// Code generated by moonctl. DO NOT EDIT.\n\n"
  let signature = "(\n  app : @moonapi.App,\n  " +
    svc +
    " : @svc.ServiceContext,\n) -> @moonapi.App {\n"
  if spec.services.length() <= 1 {
    return head +
      "///|\n/// Register every route of the " +
      spec.service +
      " service on `app`.\npub fn register_handlers" +
      signature +
      route_lines(spec.routes, st) +
      "  app\n}\n"
  }
  let mut out = head
  for s in spec.services {
    out = out +
      "///|\n/// Register the routes of the `" +
      s.name +
      "` service on `app`.\npub fn " +
      service_register(s) +
      signature +
      route_lines(s.routes, st) +
      "  app\n}\n\n"
  }
  out = out +
    "///|\n/// Register every route of every service on `app`, one service at a\ntime and in the order the spec declared them.\npub fn register_handlers" +
    signature
  for s in spec.services {
    out = out + "  let app = " + service_register(s) + "(app, svc)\n"
  }
  out + "  app\n}\n"
}

///|
/// The name of the function that registers `s`'s routes: `register_`.
fn service_register(s : @api.Service) -> String {
  "register_" + @text.to_snake(s.name)
}

///|
/// The files of `tree` a run should actually write, given a way to ask whether a
/// path is already on disk. A file moonctl owns (`Always` — the routes and the
/// types) is always written; anything else is written only when it is not there
/// yet, so a second run refreshes what follows the spec and leaves every handler and
/// logic body the author has since filled in exactly as it is.
pub fn tree_plan(
  tree : Array[TreeFile],
  exists : (String) -> Bool,
) -> Array[@plugin.GenFile] {
  let out : Array[@plugin.GenFile] = []
  for f in tree {
    if f.regen == Always || exists(f.path) == false {
      out.push({ path: f.path, content: f.content, })
    }
  }
  out
}