// 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.
pub(all) enum Regen {
  Always
  Once
} derive(Eq)

///|
/// 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
}

///|
/// A `moon.pkg.json` importing `entries` — each a `(path, alias)` pair, where an
/// empty alias leaves the path's last segment as the name — marked as the module's
/// executable when `main_`.
fn pkg_manifest(
  entries : Array[(String, String)],
  main_? : Bool = false,
) -> String {
  let lines : Array[String] = []
  for e in entries {
    lines.push(
      if e.1 == "" {
        "    " + quote(e.0)
      } else {
        "    { \"path\": " + quote(e.0) + ", \"alias\": " + quote(e.1) + " }"
      },
    )
  }
  let body = if lines.length() == 0 {
    "[]"
  } else {
    "[\n" + join_commas(lines) + "\n  ]"
  }
  let mut out = "{\n  \"import\": " + body
  if main_ {
    out = out + ",\n  \"is-main\": true,\n  \"supported-targets\": [\"native\"]"
  }
  out + "\n}\n"
}

///|
/// 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 : Route, 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.json`, `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.json` 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 : Spec,
  style? : Style,
  dir? : String = "",
) -> Array[TreeFile] {
  let st = match style {
    Some(s) => s
    None => Style::gozero()
  }
  let out : Array[TreeFile] = []
  let mod_ = spec.service
  let svc_file = st.format(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 api = ("Lfan-ke/moonapi", "")
  let asgi = ("Lfan-ke/moonasgi", "")

  // The module itself.
  out.push({
    path: at(dir, "moon.mod.json"),
    content: mod_json(mod_, [
      ("Lfan-ke/moonapi", "0.6.0"),
      ("Lfan-ke/moonasgi", "0.1.0"),
    ]),
    regen: Once,
  })
  out.push({
    path: at(dir, "moon.pkg.json"),
    content: pkg_manifest(
      [api, (config_pkg, ""), (svc_pkg, ""), (handler_pkg, "")],
      main_=true,
    ),
    regen: Once,
  })
  out.push({
    path: at(dir, svc_file + ".mbt"),
    content: "///|\n/// The " +
    spec.service +
    " service: read the configuration, build the context the handlers share, and\n/// register every route on a fresh app. What comes back is a moonasgi application —\n/// serve it with whichever server you run.\nfn main {\n  let cfg = @config.Config::new()\n  println(\"" +
    spec.service +
    " configured for \" + cfg.host + \":\" + cfg.port.to_string())\n  @handler.register_handlers(@moonapi.App::new(), @svc.ServiceContext::new(cfg))\n  |> ignore\n}\n",
    regen: Once,
  })

  // etc/.yaml — the configuration the entry point reads.
  out.push({
    path: at(dir, "etc/" + svc_file + ".yaml"),
    content: "name: " + spec.service + "\nhost: 0.0.0.0\nport: 8888\n",
    regen: Once,
  })

  // internal/config
  out.push({
    path: at(dir, "internal/config/moon.pkg.json"),
    content: pkg_manifest([]),
    regen: Once,
  })
  out.push({
    path: at(dir, "internal/config/" + st.format("config") + ".mbt"),
    content: "///|\n/// What the " +
    spec.service +
    " service reads out of `etc/" +
    svc_file +
    ".yaml`.\npub(all) struct Config {\n  host : String\n  port : Int\n}\n\n///|\n/// The configuration `etc/" +
    svc_file +
    ".yaml` ships with.\npub fn Config::new(host~ : String = \"0.0.0.0\", port~ : Int = 8888) -> Config {\n  { host, port }\n}\n",
    regen: Once,
  })

  // internal/svc
  out.push({
    path: at(dir, "internal/svc/moon.pkg.json"),
    content: pkg_manifest([(config_pkg, "")]),
    regen: Once,
  })
  out.push({
    path: at(dir, "internal/svc/" + st.format("service_context") + ".mbt"),
    content: "///|\n/// What every handler is given besides its request: the loaded configuration, and\n/// whatever else the service depends on — add those here.\npub(all) struct ServiceContext {\n  config : @config.Config\n}\n\n///|\n/// Build the context the handlers share.\npub fn ServiceContext::new(config : @config.Config) -> ServiceContext {\n  { config, }\n}\n",
    regen: Once,
  })

  // internal/types — the spec's schemas, rewritten on every run.
  out.push({
    path: at(dir, "internal/types/moon.pkg.json"),
    content: pkg_manifest([]),
    regen: Once,
  })
  out.push({
    path: at(dir, "internal/types/" + st.format("types") + ".mbt"),
    content: "// Code generated by moonctl. DO NOT EDIT.\n\n" +
    render_structs(spec.types),
    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"))
  }
  for d in group_dirs {
    routes_imports.push((handler_pkg + "/" + d, d))
  }
  out.push({
    path: at(dir, "internal/handler/moon.pkg.json"),
    content: pkg_manifest(routes_imports),
    regen: Always,
  })
  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.json"),
      content: pkg_manifest([
        api,
        asgi,
        (svc_pkg, ""),
        (logic_pkg + "/" + d, "logic"),
      ]),
      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: "///|\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  @logic." +
      st.format(r.handler + "_logic") +
      "(svc, ctx)\n}\n",
      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.json"))),
      content: pkg_manifest([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: "///|\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  _ctx : @moonapi.Context,\n) -> @moonasgi.Response {\n  @moonapi.text(200, " +
      quote("TODO: " + r.handler) +
      ")\n}\n",
      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.json"),
      content: pkg_manifest([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
}

///|
/// `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.
fn render_routes(spec : Spec, st : Style) -> String {
  let svc = if spec.routes.length() == 0 { "_svc" } else { "svc" }
  let mut out = "// Code generated by moonctl. DO NOT EDIT.\n\n///|\n/// Register every route of the " +
    spec.service +
    " service on `app`.\npub fn register_handlers(\n  app : @moonapi.App,\n  " +
    svc +
    " : @svc.ServiceContext,\n) -> @moonapi.App {\n"
  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
    }
    let d = route_dir(r, st)
    let call = (if d == "" { "" } else { "@" + d + "." }) +
      st.format(r.handler + "_handler")
    let head = "  app." +
      r.verb +
      "(" +
      quote(r.path) +
      ", ctx => " +
      call +
      "(svc, ctx)"
    out = out +
      (if r.summary != "" {
        head + ", summary=" + quote(r.summary) + ")\n"
      } else {
        head + ")\n"
      })
  }
  out + "  app\n}\n"
}

///|
/// 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[GenFile] {
  let out : Array[GenFile] = []
  for f in tree {
    if f.regen == Always || exists(f.path) == false {
      out.push({ path: f.path, content: f.content, })
    }
  }
  out
}