///|
/// Converts the root JSON object into a typed loaded manifest.
fn build_loaded_manifest_from_json(
  json : Json,
  base_dir : String?,
  allow_extensions? : Bool = false,
) -> LoadedAppManifest raise BootstrapError {
  let object = expect_object(json, "app manifest")
  LoadedAppManifest::new(
    build_app_manifest_from_object(object, base_dir, allow_extensions~),
    base_dir~,
  )
}

///|
/// Builds a manifest window config from a JSON object.
fn build_window_manifest_from_json(
  json : Json,
) -> @manifest.WindowManifest raise BootstrapError {
  build_window_manifest_from_object(expect_object(json, "window manifest"))
}

///|
/// Builds a typed manifest from the root manifest object fields.
fn build_app_manifest_from_object(
  object : Map[String, Json],
  base_dir : String?,
  allow_extensions? : Bool = false,
) -> @manifest.AppManifest raise BootstrapError {
  let window = build_required_window_from_object(object)
  let entry = build_required_entry_from_object(object, base_dir)
  let windows = build_optional_windows_from_object(object, base_dir)
  let extensions = build_declared_extension_settings_from_object(
    object,
    allow_extensions~,
  )
  let permissions = build_permission_grants_from_object(object, windows)
  let debug = build_debug_value_from_object(object)
  @manifest.AppManifest::new(
    window,
    entry,
    debug=int_or_default(debug, 0),
    windows~,
    extensions~,
    permissions~,
  )
}

///|
/// Builds the required primary window field from the root manifest object.
fn build_required_window_from_object(
  object : Map[String, Json],
) -> @manifest.WindowManifest raise BootstrapError {
  required_built_field(
    object, "window", "app manifest.window", build_window_manifest_from_json,
  )
}

///|
/// Builds the required primary entry field from the root manifest object.
fn build_required_entry_from_object(
  object : Map[String, Json],
  base_dir : String?,
) -> @manifest.AppEntry raise BootstrapError {
  required_built_field(object, "entry", "app manifest.entry", entry_json => {
    build_app_entry_from_json(entry_json, base_dir)
  })
}

///|
/// Builds the optional secondary windows field from the root manifest object.
fn build_optional_windows_from_object(
  object : Map[String, Json],
  base_dir : String?,
) -> Array[@manifest.AppWindowManifest] raise BootstrapError {
  build_app_windows_from_json(optional_field(object, "windows"), base_dir)
}

///|
/// Builds extension settings for internal runtime manifests.
fn build_declared_extension_settings_from_object(
  object : Map[String, Json],
  allow_extensions? : Bool = false,
) -> Map[String, @manifest.ExtensionSetting] raise BootstrapError {
  if !allow_extensions && object.get("extensions") is Some(_) {
    raise UnsupportedField(
      path="app manifest.extensions",
      reason="declare extensions in MoonBit code with .extension(...)",
    )
  }
  build_extension_settings(optional_field(object, "extensions"))
}

///|
/// Builds the optional debug field from the root manifest object.
fn build_debug_value_from_object(
  object : Map[String, Json],
) -> Int? raise BootstrapError {
  decode_optional_int(optional_field(object, "debug"), "app manifest.debug")
}

///|
/// Builds a manifest window config from an already validated object.
fn build_window_manifest_from_object(
  object : Map[String, Json],
) -> @manifest.WindowManifest raise BootstrapError {
  let title = required_string_field(object, "title", "window.title")
  let width = required_int_field(object, "width", "window.width")
  let height = required_int_field(object, "height", "window.height")
  let size_hint = parse_size_hint(
    decode_optional_string(
      optional_field(object, "size_hint"),
      "window.size_hint",
    ),
  )
  let titlebar_style = parse_titlebar_style(
    decode_optional_string(
      optional_field(object, "titlebar_style"),
      "window.titlebar_style",
    ),
  )
  @manifest.WindowManifest::new(
    title,
    width,
    height,
    size_hint~,
    titlebar_style~,
  )
}

///|
/// Builds an app entry value and rebases relative file-backed entries against the config directory.
fn build_app_entry_from_json(
  json : Json,
  base_dir : String?,
) -> @manifest.AppEntry raise BootstrapError {
  let object = expect_object(json, "app entry")
  build_app_entry_from_parts(
    required_string_field(object, "kind", "entry.kind"),
    required_string_field(object, "value", "entry.value"),
    base_dir,
  )
}

///|
/// Builds an app entry from decoded `kind` and `value` fields.
fn build_app_entry_from_parts(
  kind : String,
  value : String,
  base_dir : String?,
) -> @manifest.AppEntry raise BootstrapError {
  let normalized_kind = normalize_keyword(kind)
  let rebased_value = match normalized_kind {
    "file" => rebase_relative_path(value, base_dir)
    "asset" => rebase_relative_path(value, base_dir)
    _ => value
  }
  match normalized_kind {
    "html" => @manifest.AppEntry::Html(rebased_value)
    "url" => @manifest.AppEntry::Url(rebased_value)
    "file" => @manifest.AppEntry::File(rebased_value)
    "asset" => @manifest.AppEntry::Asset(rebased_value)
    _ => raise UnsupportedValue(path="app entry kind", value=kind)
  }
}

///|
/// Builds the optional secondary window list from JSON.
fn build_app_windows_from_json(
  json : Json?,
  base_dir : String?,
) -> Array[@manifest.AppWindowManifest] raise BootstrapError {
  match json {
    None => []
    Some(Array(items)) => build_app_windows_from_array(items, base_dir)
    Some(_) =>
      raise InvalidField(
        path="app manifest.windows",
        expectation="must be an array",
      )
  }
}

///|
/// Builds every secondary window from the decoded `windows` array.
fn build_app_windows_from_array(
  items : Array[Json],
  base_dir : String?,
) -> Array[@manifest.AppWindowManifest] raise BootstrapError {
  let windows : Array[@manifest.AppWindowManifest] = []
  for index, item in items {
    windows.push(build_app_window_from_json(item, base_dir, index))
  }
  windows
}

///|
/// Builds one secondary window config entry from the `windows` array.
fn build_app_window_from_json(
  json : Json,
  base_dir : String?,
  index : Int,
) -> @manifest.AppWindowManifest raise BootstrapError {
  let label = "windows[" + index.to_string() + "]"
  build_app_window_from_object(expect_object(json, label), base_dir, label)
}

///|
/// Builds one secondary window config entry from an already validated object.
fn build_app_window_from_object(
  object : Map[String, Json],
  base_dir : String?,
  label : String,
) -> @manifest.AppWindowManifest raise BootstrapError {
  let id = required_string_field(object, "id", label + ".id")
  let window = required_built_field(
    object,
    "window",
    label + ".window",
    build_window_manifest_from_json,
  )
  let entry = required_built_field(object, "entry", label + ".entry", entry_json => {
    build_app_entry_from_json(entry_json, base_dir)
  })
  let open_on_start = match optional_field(object, "open_on_start") {
    None => true
    Some(True) => true
    Some(False) => false
    Some(_) =>
      raise InvalidField(
        path=label + ".open_on_start",
        expectation="must be a boolean",
      )
  }
  @manifest.AppWindowManifest::new(id, window, entry, open_on_start~)
}

///|
/// Builds the normalized extension settings map from the `extensions` object.
fn build_extension_settings(
  json : Json?,
) -> Map[String, @manifest.ExtensionSetting] raise BootstrapError {
  match json {
    None => Map([])
    Some(Object(object)) => build_extension_settings_from_object(object)
    Some(_) =>
      raise InvalidField(
        path="app manifest.extensions",
        expectation="must be an object",
      )
  }
}

///|
/// Builds the normalized extension settings map from an already validated object.
fn build_extension_settings_from_object(
  object : Map[String, Json],
) -> Map[String, @manifest.ExtensionSetting] raise BootstrapError {
  let settings : Map[String, @manifest.ExtensionSetting] = Map([])
  for name, value in object {
    guard normalize_keyword(name) != "" else {
      raise InvalidField(
        path="app manifest.extensions",
        expectation="must not contain an empty key",
      )
    }
    settings.set(name, build_extension_setting_from_json(name, value))
  }
  settings
}

///|
/// Builds one normalized extension setting from the raw JSON value.
///
/// An extension entry only records enablement. Extension-specific configuration
/// belongs to the permission scope, which is declared per grant and validated by
/// the owning extension, so an object entry is rejected rather than silently
/// discarded.
fn build_extension_setting_from_json(
  name : String,
  value : Json,
) -> @manifest.ExtensionSetting raise BootstrapError {
  match value {
    True => @manifest.ExtensionSetting::enabled()
    False => @manifest.ExtensionSetting::disabled()
    _ =>
      raise InvalidField(
        path="app manifest.extensions.\{name}",
        expectation="must be a boolean; declare extension configuration in the matching permission scope",
      )
  }
}

///|
fn build_permission_grants_from_object(
  object : Map[String, Json],
  windows : Array[@manifest.AppWindowManifest],
) -> Array[@manifest.PermissionGrant] raise BootstrapError {
  let values = match optional_field(object, "permissions") {
    None => return []
    Some(Array(values)) => values
    Some(_) =>
      raise InvalidField(
        path="app manifest.permissions",
        expectation="must be an array",
      )
  }
  let window_ids : Map[String, Unit] = { "main": () }
  for window in windows {
    window_ids[window.id] = ()
  }
  let identities : Map[String, Unit] = Map([])
  let grants : Array[@manifest.PermissionGrant] = []
  for index, value in values {
    let path = "app manifest.permissions[" + index.to_string() + "]"
    let grant = expect_object(value, path)
    reject_unknown_permission_fields(grant, path)
    let window = required_string_field(grant, "window", path + ".window")
      .trim()
      .to_owned()
    guard window_ids.contains(window) else {
      raise InvalidField(
        path=path + ".window",
        expectation="must name main or a declared secondary window",
      )
    }
    let origin_text = required_string_field(grant, "origin", path + ".origin")
      .trim()
      .to_owned()
    let origin = match origin_text {
      "app" => @manifest.PermissionOrigin::App
      "entry" => @manifest.PermissionOrigin::Entry
      _ =>
        raise InvalidField(
          path=path + ".origin",
          expectation="must be app or entry",
        )
    }
    let extension = required_string_field(
        grant,
        "extension",
        path + ".extension",
      )
      .trim()
      .to_owned()
    guard extension != "" else {
      raise InvalidField(
        path=path + ".extension",
        expectation="must not be empty",
      )
    }
    let identity = window + "\u{0}" + origin_text + "\u{0}" + extension
    guard !identities.contains(identity) else {
      raise InvalidField(
        path~,
        expectation="duplicates an earlier window/origin/extension grant",
      )
    }
    identities[identity] = ()
    let scope = match optional_field(grant, "scope") {
      None => Json::empty_object()
      Some(Object(scope)) => Json::object(clone_json_object(scope))
      Some(_) =>
        raise InvalidField(
          path=path + ".scope",
          expectation="must be an object",
        )
    }
    grants.push(
      @manifest.PermissionGrant::new(window, origin, extension, scope~),
    )
  }
  grants
}

///|
fn reject_unknown_permission_fields(
  object : Map[String, Json],
  path : String,
) -> Unit raise BootstrapError {
  for key, _ in object {
    guard key == "window" ||
      key == "origin" ||
      key == "extension" ||
      key == "scope" else {
      raise InvalidField(
        path=path + "." + key,
        expectation="is not a supported permission field",
      )
    }
  }
}