///|
async fn validate_bundle(layout : ProjectLayout, target : ServerTarget) -> Unit {
  guard target is Native else {
    fail("--bundle requires --server-target native.")
  }
  if layout.server_entry is Some(entry) {
    let stub = source_path_to_string(entry.join("warren_assets.mbt"))
    guard @fs.exists(stub) && @fs.kind(stub, follow_symlink=false) is Regular else {
      fail(
        "Server entry is not configured for bundling: missing warren_assets.mbt. For new projects use `warren new  --template server`; for existing projects follow the server migration steps in Warren's README.",
      )
    }
  }
  if layout.public_dir is Some(dir) {
    validate_bundle_tree(dir)
  }
}

///|
// Reject links and special files before copying: following a directory link can
// otherwise recurse forever or accidentally embed files outside public/.
async fn validate_bundle_tree(dir : SourcePath) -> Unit {
  for
    name in @fs.readdir(
      source_path_to_string(dir),
      include_hidden=true,
      sort=true,
    ) {
    let path = dir.join(name)
    match @fs.kind(source_path_to_string(path), follow_symlink=false) {
      Regular => ()
      Directory => validate_bundle_tree(path)
      _ => fail("Bundle inputs must be regular files or directories: `\{path}`")
    }
  }
}

///|
fn bundle_bytes_expression(bytes : Bytes) -> String {
  guard bytes.length() > 4096 else { bytes.to_string() }
  // A multiline Bytes literal compiles to static storage, without rebuilding
  // the resource on the heap. Keep source lines short for large binary files.
  let out = StringBuilder()
  out.write_string("[\n")
  for i, byte in bytes {
    if i % 4096 == 0 {
      out.write_string("      ")
    }
    out.write_string("0x\{byte.to_hex()}, ")
    if i % 4096 == 4095 {
      out.write_string("\n")
    }
  }
  out.write_string("\n    ]")
  out.to_string()
}

///|
async fn render_bundle_assets(dir : SourcePath) -> String {
  let out = StringBuilder()
  out.write_string(
    "// Generated by warren build --bundle.\n///|\nfn warren_assets() -> Map[String, Bytes] {\n  {\n",
  )
  async fn append_files(folder : SourcePath, prefix : String) -> Unit {
    for
      name in @fs.readdir(
        source_path_to_string(folder),
        include_hidden=true,
        sort=true,
      ) {
      let file = folder.join(name)
      let key = "\{prefix}/\{name}"
      match @fs.kind(source_path_to_string(file), follow_symlink=false) {
        Directory => append_files(file, key)
        Regular => {
          let bytes = @fs.read_file(source_path_to_string(file)).binary()
          out.write_string(
            "    \{key.escape()}: \{bundle_bytes_expression(bytes)},\n",
          )
        }
        _ => fail("Cannot embed non-regular file: `\{file}`")
      }
    }
  }
  append_files(dir, "")
  out.write_string("  }\n}\n")
  out.to_string()
}

///|
async fn copy_bundle_entry(src : SourcePath, dst : SourcePath) -> Unit {
  for
    name in @fs.readdir(
      source_path_to_string(src),
      include_hidden=true,
      sort=true,
    ) {
    if name == "_build" ||
      name == ".git" ||
      name == ".mooncakes" ||
      name == "dist" {
      continue
    }
    let file = src.join(name)
    match @fs.kind(source_path_to_string(file), follow_symlink=false) {
      Directory => {
        @fs.mkdir(source_path_to_string(dst.join(name)), permission=0o700)
        copy_bundle_entry(file, dst.join(name))
      }
      Regular => {
        copy(src=file, dst=dst.join(name))
        if @fs.can_execute(source_path_to_string(file)) {
          chmod_executable(dst.join(name))
        }
      }
      _ =>
        fail(
          "Bundle server sources must not contain links or special files: `\{file}`",
        )
    }
  }
}

///|
async fn build_bundle(
  layout : ProjectLayout,
  target : ServerTarget,
  dist : SourcePath,
) -> Unit {
  validate_bundle(layout, target)
  let temp_string = @fs.tmpdir(prefix="warren-bundle")
  defer @fs.rmdir(temp_string, recursive=true)
  let temp = SourcePath::new(temp_string)
  // Keep generated server artifacts under the same success/failure cleanup.
  let target_dir = temp.join("build")
  let browser_artifact = run_build_only(
    layout.root,
    layout.browser_entry,
    "js",
    true,
  )
  let static_dir = temp.join("static")
  assemble_static(
    layout.root,
    layout.public_dir,
    browser_artifact,
    static_dir,
    true,
  )
  let source = render_bundle_assets(static_dir)
  if layout.server_entry is Some(entry) {
    // A sibling copy preserves both module-relative build rules and relative
    // paths to neighboring native stubs/libraries in the entry manifest.
    let parent = SourcePath::new(
      Path::dirname(source_path_to_string(entry)).to_string(),
    )
    let unique_name = Path::basename(temp_string)
      .to_owned()
      .replace_all(old=".", new="_")
      .replace_all(old="-", new="_")
    let staging = parent.join(unique_name)
    @fs.mkdir(source_path_to_string(staging), permission=0o700)
    defer @fs.rmdir(source_path_to_string(staging), recursive=true)
    copy_bundle_entry(entry, staging)
    @fs.write_file(
      source_path_to_string(staging.join("warren_assets.mbt")),
      source,
      create_mode=CreateOrTruncate,
      permission=0o600,
    )
    let artifact = run_build_only(
      layout.root,
      staging,
      "native",
      true,
      target_dir~,
    )
    let artifact_name = Path::basename(source_path_to_string(artifact)).to_owned()
    let entry_name = Path::basename(source_path_to_string(entry)).to_owned()
    let output_name = if artifact_name.has_prefix(unique_name) {
      "\{entry_name}\{artifact_name[unique_name.length():]}"
    } else {
      artifact_name
    }
    ensure_clean_dir(dist)
    ignore(copy_server_artifact(artifact, dist, Native, output_name~))
  } else {
    let entry = temp.join("server.mbtx")
    @fs.write_file(
      source_path_to_string(entry),
      bundle_server_source + "\n" + source,
      create_mode=CreateOrTruncate,
      permission=0o600,
    )
    let artifact = run_build_only(
      layout.root,
      entry,
      "native",
      true,
      target_dir~,
    )
    ensure_clean_dir(dist)
    ignore(copy_server_artifact(artifact, dist, Native))
  }
  log("build", "Bundled executable written to \{dist}")
}