///|
fn platform_entrypoint_directory(platform : String) -> String {
  if platform == "web" {
    "web_wasm"
  } else if is_mobile_platform(platform) {
    platform + "_window_hosted"
  } else {
    platform + "_skia"
  }
}

///|
fn platform_files_for(
  options : ProjectOptions,
  platform : String,
) -> Result[Array[PlannedFile], String] {
  if !is_platform(platform) {
    return Err("unsupported platform: " + platform)
  }
  if platform == "web" {
    return Ok([
      text_file("web_wasm/moon.pkg", web_pkg_text(options.module_name)),
      text_file("web_wasm/main.mbt", web_main_text(options.title)),
      text_file("web_wasm/index.html", web_html_text(options.title)),
    ])
  }
  if platform == "macos" || platform == "windows" || platform == "linux" {
    let directory = platform_entrypoint_directory(platform)
    return Ok([
      text_file(
        directory + "/moon.pkg",
        desktop_pkg_text(options.module_name, platform),
      ),
      text_file(
        directory + "/main.mbt",
        desktop_main_text(options.title, platform),
      ),
    ])
  }
  if is_mobile_platform(platform) {
    let directory = platform_entrypoint_directory(platform)
    return Ok([
      text_file(
        directory + "/moon.pkg",
        mobile_entrypoint_pkg_text(options.module_name, platform),
      ),
      text_file(directory + "/main.mbt", mobile_entrypoint_main_text(platform)),
    ])
  }
  Err("unsupported platform: " + platform)
}

///|
fn merge_platform_lists(
  existing : Array[String],
  requested : Array[String],
) -> Result[(Array[String], Array[String]), String] {
  let selected : Map[String, Bool] = Map([])
  for platform in existing {
    if !is_platform(platform) {
      return Err("existing project contains unsupported platform: " + platform)
    }
    selected[platform] = true
  }
  let added : Array[String] = []
  for requested_platform in requested {
    let platform = requested_platform.to_lower()
    if platform == "all" {
      for value in platform_order {
        if !selected.contains(value) {
          selected[value] = true
          added.push(value)
        }
      }
    } else if is_platform(platform) {
      if !selected.contains(platform) {
        selected[platform] = true
        added.push(platform)
      }
    } else {
      return Err(
        "unsupported platform: " +
        requested_platform +
        " (expected web|macos|windows|linux|android|ios|harmonyos|all)",
      )
    }
  }
  let merged : Array[String] = []
  for platform in platform_order {
    if selected.contains(platform) {
      merged.push(platform)
    }
  }
  Ok((merged, added))
}

///|
fn merge_mobile_config_for_platforms(
  existing_text : String?,
  options : ProjectOptions,
) -> Result[String, String] {
  if !options.platforms.any(is_mobile_platform) {
    return Ok(existing_text.unwrap_or(""))
  }
  match existing_text {
    None | Some("") => Ok(mobile_config_text(options))
    Some(text) => {
      let parsed = @json.parse(text) catch {
        _ => return Err("moui.mobile.json is invalid JSON")
      }
      guard parsed is Object(root) else {
        return Err("moui.mobile.json root must be an object")
      }
      let generated = @json.parse(mobile_config_text(options)) catch {
        _ => return Err("could not build mobile platform defaults")
      }
      guard generated is Object(generated_root) else {
        return Err("generated mobile config must be an object")
      }
      for platform in ["android", "ios", "harmonyos"] {
        if options.platforms.contains(platform) && !root.contains(platform) {
          match generated_root.get(platform) {
            Some(value) => root[platform] = value
            None => ()
          }
        }
      }
      if !root.contains("schemaVersion") {
        root["schemaVersion"] = (1 : Int).to_json()
      }
      Ok(Json::object(root).stringify(indent=2) + "\n")
    }
  }
}

///|
fn write_planned_files_into(
  project_root : String,
  files : Array[PlannedFile],
) -> Result[Array[String], String] {
  match validate_file_plan(files) {
    Ok(_) => ()
    Err(message) => return Err(message)
  }
  let written : Array[String] = []
  for file in files {
    let destination = join_path(project_root, file.path)
    if @fs.path_exists(destination) {
      return Err("refusing to overwrite existing file: " + file.path)
    }
    match mkdir_p(parent_dir(destination)) {
      Ok(_) => ()
      Err(message) => return Err(message)
    }
    @fs.write_bytes_to_file(destination, file.bytes) catch {
      _ => return Err("could not write generated file: " + file.path)
    }
    written.push(file.path)
  }
  Ok(written)
}

///|
fn add_platforms_to_project(
  options : AddPlatformOptions,
) -> Result[(Array[String], Array[String]), String] {
  let manifest = match read_project_manifest(options.project_root) {
    Ok(value) => value
    Err(message) => return Err(message)
  }
  let (merged, added) = match
    merge_platform_lists(manifest.platforms, options.platforms) {
    Ok(value) => value
    Err(message) => return Err(message)
  }
  if added.is_empty() {
    return Err("project already includes the requested platforms")
  }
  let bundle_id = match options.bundle_id {
    Some(value) => Some(value)
    None => manifest.bundle_id
  }
  let project_options : ProjectOptions = {
    target: options.project_root,
    id: manifest.id,
    module_name: manifest.module_name,
    title: manifest.title,
    moui_version: manifest.moui_version,
    moui_skia_renderer_version: default_moui_skia_renderer_dependency_version,
    template: "counter",
    platforms: merged,
    bundle_id,
  }
  match validate_project_options(project_options) {
    Ok(_) => ()
    Err(message) => return Err(message)
  }
  let files : Array[PlannedFile] = []
  for platform in added {
    match platform_files_for(project_options, platform) {
      Ok(platform_files) =>
        for file in platform_files {
          files.push(file)
        }
      Err(message) => return Err(message)
    }
  }
  let updated_manifest : ProjectManifest = {
    schema_version: 1,
    id: manifest.id,
    title: manifest.title,
    module_name: manifest.module_name,
    moui_version: manifest.moui_version,
    platforms: merged.copy(),
    bundle_id,
  }
  match
    replace_file_atomic(
      join_path(options.project_root, "moui.project.json"),
      @utf8.encode(updated_manifest.to_json_text()[:], bom=false),
    ) {
    Ok(_) => ()
    Err(message) => return Err(message)
  }
  if merged.any(is_mobile_platform) {
    let mobile_path = join_path(options.project_root, "moui.mobile.json")
    let existing = if @fs.path_exists(mobile_path) {
      let text = @fs.read_file_to_string(mobile_path) catch {
        _ => return Err("could not read moui.mobile.json: " + mobile_path)
      }
      Some(text)
    } else {
      None
    }
    let mobile_text = match
      merge_mobile_config_for_platforms(existing, project_options) {
      Ok(value) => value
      Err(message) => return Err(message)
    }
    if mobile_text != "" {
      match
        replace_file_atomic(
          mobile_path,
          @utf8.encode(mobile_text[:], bom=false),
        ) {
        Ok(_) => ()
        Err(message) => return Err(message)
      }
    }
  }
  let written = match write_planned_files_into(options.project_root, files) {
    Ok(value) => value
    Err(message) => return Err(message)
  }
  // Keep README current when present or create it when absent.
  let readme_path = join_path(options.project_root, "README.md")
  let readme = readme_text(project_options)
  if @fs.path_exists(readme_path) {
    match replace_file_atomic(readme_path, @utf8.encode(readme[:], bom=false)) {
      Ok(_) => ()
      Err(message) => return Err(message)
    }
  } else {
    match
      write_planned_files_into(options.project_root, [
        text_file("README.md", readme),
      ]) {
      Ok(_) => ()
      Err(message) => return Err(message)
    }
  }
  Ok((added, written))
}